diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000000..accab46dc6f5 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include README.rst +recursive-include warehouse/templates *.html +recursive-include warehouse/templates *.txt diff --git a/README.rst b/README.rst index 3e0ae077d1cf..13e449d81dde 100644 --- a/README.rst +++ b/README.rst @@ -35,8 +35,22 @@ running locally and are configured to not require passwords. # If you want to use the default development configuration $ warehouse runserver --settings=configs.dev --configuration=Development -5. Run the tests + +Running the tests +----------------- + +Warehouse uses pytest to run the test suite. You can run all the tests by using: .. code:: bash $ py.test + +Unit and functional tests have been marked and you may run either of by using: + +..code:: bash + + # Run only the unit tests + $ py.test -m unit + + # Run only the functional tests + $ py.test -m functional diff --git a/configs/dev.py b/configs/dev.py index c93eb61a67f8..1167a0758a1a 100644 --- a/configs/dev.py +++ b/configs/dev.py @@ -21,6 +21,8 @@ class Development(Common): DEBUG = True TEMPLATE_DEBUG = True + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + class Testing(Common): diff --git a/setup.py b/setup.py index f143083bec1c..4946037db87e 100644 --- a/setup.py +++ b/setup.py @@ -24,13 +24,23 @@ ], extras_require={ "tests": [ + "django-webtest", + "pretend", "pytest", "pytest-cov", "pytest-django>=2.3.0", + "webtest", ], }, packages=find_packages(exclude=["tests"]), + package_data={ + "warehouse": [ + "templates/*.html", + "templates/*.txt", + ], + }, + include_package_data=True, entry_points={ "console_scripts": [ diff --git a/tests/accounts/test_forms.py b/tests/accounts/test_forms.py deleted file mode 100644 index 95e353d54ced..000000000000 --- a/tests/accounts/test_forms.py +++ /dev/null @@ -1,10 +0,0 @@ -from warehouse.accounts.forms import UserChangeForm - - -def test_user_change_form_initalizes(): - UserChangeForm() - - -def test_user_change_form_clean_password(): - form = UserChangeForm({"password": "fail"}, initial={"password": "epic"}) - assert form.clean_password() == "epic" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000000..7baa9c8cb823 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,53 @@ +import pytest + + +def pytest_collection_modifyitems(items): + for item in items: + path, name = item.nodeid.split("::") + if path.startswith("tests/unit/"): + item.keywords["unit"] = pytest.mark.unit + elif path.startswith("tests/functional"): + item.keywords["functional"] = pytest.mark.functional + + +@pytest.fixture +def webtest(request): + from django.conf import settings + from django.test.utils import override_settings + + from django_webtest import DjangoTestApp + + middleware = list(settings.MIDDLEWARE_CLASSES) + django_auth_middleware = ("django.contrib.auth.middleware." + "AuthenticationMiddleware") + webtest_auth_middleware = "django_webtest.middleware.WebtestUserMiddleware" + if django_auth_middleware not in settings.MIDDLEWARE_CLASSES: + # There can be a custom AuthenticationMiddleware subclass or + # replacement, we can't compute its index so just put our auth + # middleware to the end. + middleware.append(webtest_auth_middleware) + else: + index = middleware.index(django_auth_middleware) + middleware.insert(index+1, webtest_auth_middleware) + + auth_backends = ["django_webtest.backends.WebtestUserBackend"] + auth_backends += list(settings.AUTHENTICATION_BACKENDS) + + patched_settings = override_settings( + # We want exceptions to be raised naturally so that + # they are treated like normal exceptions in a + # test case. + DEBUG_PROPAGATE_EXCEPTIONS=True, + # We want to insert the django-webtest middleware + # into our middleware classes so that we can + # auth from a webtest easily. + MIDDLEWARE_CLASSES=middleware, + # We want to insert the django-webtest + # authentication backend so that webtest can + # easily authenticate. + AUTHENTICATION_BACKENDS=auth_backends, + ) + patched_settings.enable() + request.addfinalizer(patched_settings.disable) + + return DjangoTestApp() diff --git a/tests/accounts/__init__.py b/tests/functional/__init__.py similarity index 100% rename from tests/accounts/__init__.py rename to tests/functional/__init__.py diff --git a/tests/management/__init__.py b/tests/functional/accounts/__init__.py similarity index 100% rename from tests/management/__init__.py rename to tests/functional/accounts/__init__.py diff --git a/tests/functional/accounts/test_signup.py b/tests/functional/accounts/test_signup.py new file mode 100644 index 000000000000..1edcd3e4ff78 --- /dev/null +++ b/tests/functional/accounts/test_signup.py @@ -0,0 +1,15 @@ +import pytest + +from django.core.urlresolvers import reverse + + +@pytest.mark.django_db(transaction=True) +def test_simple_signup(webtest): + form = webtest.get(reverse("accounts.signup")).form + form["username"] = "testuser" + form["email"] = "testuser@example.com" + form["password"] = "test password!" + form["confirm_password"] = "test password!" + response = form.submit() + + assert response.status_code == 303 diff --git a/tests/management/commands/__init__.py b/tests/unit/__init__.py similarity index 100% rename from tests/management/commands/__init__.py rename to tests/unit/__init__.py diff --git a/tests/unit/accounts/__init__.py b/tests/unit/accounts/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/unit/accounts/test_adapters.py b/tests/unit/accounts/test_adapters.py new file mode 100644 index 000000000000..f554b9fc11a0 --- /dev/null +++ b/tests/unit/accounts/test_adapters.py @@ -0,0 +1,148 @@ +from pretend import stub +from unittest import mock + +import pytest + +from warehouse.accounts.adapters import Email, EmailAdapter, User, UserAdapter + + +def test_useradapter_no_username(): + with pytest.raises(ValueError): + UserAdapter().create(None) + + +def test_useradapter_creates(): + created = mock.NonCallableMock() + created.username = "testuser" + created.set_password = mock.Mock() + created.save = mock.Mock() + model = mock.Mock(return_value=created) + + adapter = UserAdapter() + adapter.model = model + + user = adapter.create("testuser", "testpassword") + + assert user.username == "testuser" + + assert model.call_count == 1 + assert model.call_args == (tuple(), { + "username": "testuser", + "is_staff": False, + "is_superuser": False, + "is_active": True, + "last_login": mock.ANY, + "date_joined": mock.ANY, + }) + + assert created.set_password.call_count == 1 + assert created.set_password.call_args == (("testpassword",), {}) + + assert created.save.call_count == 1 + assert created.save.call_args == (tuple(), {}) + + +@pytest.mark.parametrize(("exists",), [(True,), (False,)]) +def test_useradapter_username_exists(exists): + mexists = mock.Mock(return_value=exists) + mfilter = mock.Mock(return_value=stub(exists=mexists)) + model = stub(objects=stub(filter=mfilter)) + + adapter = UserAdapter() + adapter.model = model + + uexists = adapter.username_exists("testuser") + + assert uexists == exists + + assert mfilter.call_count == 1 + assert mfilter.call_args == (tuple(), {"username": "testuser"}) + + assert mexists.call_count == 1 + assert mexists.call_args == (tuple(), {}) + + +def test_useradapter_serializer(): + adapter = UserAdapter() + + user = adapter._serialize(stub(username="testuser")) + + assert isinstance(user, User) + assert user == ("testuser",) + assert user.username == "testuser" + + +@pytest.mark.parametrize(("primary", "verified"), [ + (None, None), + (None, True), + (None, False), + (True, True), + (True, False), + (True, None), + (False, True), + (False, False), + (False, None), +]) +def test_emailadapter_creates(primary, verified): + user = stub(username="testuser") + user_model_get = mock.Mock(return_value=user) + user_model = stub(objects=stub(get=user_model_get)) + + created_model_save = mock.Mock() + created_model = stub( + user=user, + email="test@example.com", + primary=primary if primary is not None else False, + verified=verified if verified is not None else False, + save=created_model_save, + ) + email_model = mock.Mock(return_value=created_model) + + adapter = EmailAdapter(user=user_model) + adapter.model = email_model + + kwargs = {} + if primary is not None: + kwargs["primary"] = primary + if verified is not None: + kwargs["verified"] = verified + + email = adapter.create("testuser", "test@example.com", **kwargs) + + primary = primary if primary is not None else False + verified = verified if verified is not None else False + + assert email.user == "testuser" + assert email.email == "test@example.com" + + assert user_model_get.call_count == 1 + assert user_model_get.call_args == (tuple(), {"username": "testuser"}) + + assert email_model.call_count == 1 + assert email_model.call_args == (tuple(), { + "user": user, + "email": "test@example.com", + "primary": primary, + "verified": verified, + }) + + assert created_model_save.call_count == 1 + assert created_model_save.call_args == (tuple(), {}) + + +def test_emailadapter_serializer(): + adapter = EmailAdapter(user=None) + + email = adapter._serialize(stub( + user=stub(username="testuser"), + email="test@example.com", + primary=True, + verified=False, + )) + + assert isinstance(email, Email) + assert email == ("testuser", "test@example.com", True, False) + assert email.user == "testuser" + assert email.email == "test@example.com" + assert email.primary + assert not email.verified diff --git a/tests/unit/accounts/test_forms.py b/tests/unit/accounts/test_forms.py new file mode 100644 index 000000000000..29b8dff396a3 --- /dev/null +++ b/tests/unit/accounts/test_forms.py @@ -0,0 +1,68 @@ +from unittest import mock +from pretend import stub + +import pytest + +from django import forms + +from warehouse.accounts.forms import SignupForm, UserChangeForm + + +def test_signup_form_initalizes(): + SignupForm() + + +def test_signup_form_clean_username_valid(): + username_exists = mock.Mock(return_value=False) + model = stub(api=stub(username_exists=username_exists)) + form = SignupForm({"username": "testuser"}) + form.cleaned_data = {"username": "testuser"} + form.model = model + + cleaned = form.clean_username() + + assert cleaned == "testuser" + assert username_exists.call_count == 1 + assert username_exists.call_args == (("testuser",), {}) + + +def test_signup_form_clean_username_invalid(): + username_exists = mock.Mock(return_value=True) + model = stub(api=stub(username_exists=username_exists)) + form = SignupForm({"username": "testuser"}) + form.cleaned_data = {"username": "testuser"} + form.model = model + + with pytest.raises(forms.ValidationError): + form.clean_username() + + assert username_exists.call_count == 1 + assert username_exists.call_args == (("testuser",), {}) + + +def test_signup_form_clean_passwords_valid(): + data = {"password": "test password", "confirm_password": "test password"} + form = SignupForm(data) + form.cleaned_data = data + + cleaned = form.clean_confirm_password() + + assert cleaned == "test password" + + +def test_signup_form_clean_passwords_invalid(): + data = {"password": "test password", "confirm_password": "different!"} + form = SignupForm(data) + form.cleaned_data = data + + with pytest.raises(forms.ValidationError): + form.clean_confirm_password() + + +def test_user_change_form_initalizes(): + UserChangeForm() + + +def test_user_change_form_clean_password(): + form = UserChangeForm({"password": "fail"}, initial={"password": "epic"}) + assert form.clean_password() == "epic" diff --git a/tests/accounts/test_models.py b/tests/unit/accounts/test_models.py similarity index 100% rename from tests/accounts/test_models.py rename to tests/unit/accounts/test_models.py diff --git a/tests/unit/accounts/test_regards.py b/tests/unit/accounts/test_regards.py new file mode 100644 index 000000000000..bbfb9aa2662f --- /dev/null +++ b/tests/unit/accounts/test_regards.py @@ -0,0 +1,30 @@ +from pretend import stub +from unittest import mock + +from warehouse.accounts.regards import UserCreator + + +def test_user_creator_basic(): + UserCreator() + + +def test_user_creator(): + user_creator = mock.Mock(return_value=stub(username="testuser")) + email_creator = mock.Mock(return_value=stub(email="test@example.com")) + mailer = mock.Mock() + + creator = UserCreator( + user_creator=user_creator, + email_creator=email_creator, + mailer=mailer, + ) + + user = creator("testuser", "test@example.com", "testpassword") + + assert user.username == "testuser" + + assert user_creator.call_count == 1 + assert user_creator.call_args == (("testuser", "testpassword"), {}) + + assert email_creator.call_count == 1 + assert email_creator.call_args == (("testuser", "test@example.com"), {}) diff --git a/tests/unit/accounts/test_views.py b/tests/unit/accounts/test_views.py new file mode 100644 index 000000000000..83bce5cdd434 --- /dev/null +++ b/tests/unit/accounts/test_views.py @@ -0,0 +1,107 @@ +import pytest + +from unittest import mock +from pretend import stub + +from django.conf import settings +from django.core.urlresolvers import reverse +from django.shortcuts import resolve_url + +from warehouse.accounts.forms import SignupForm +from warehouse.accounts.views import SignupView + + +def test_signup_flow(rf): + class TestSignupForm(SignupForm): + model = stub(api=stub(username_exists=lambda x: False)) + + creator = mock.Mock() + view = SignupView.as_view(creator=creator, form_class=TestSignupForm) + + # Verify that we can GET the signup url + request = rf.get(reverse("accounts.signup")) + response = view(request) + + assert response.status_code == 200 + assert response.context_data.keys() == set(["next", "form"]) + assert response.context_data["next"] is None + assert isinstance(response.context_data["form"], SignupForm) + + # Attempt to create the user and verify it worked + data = { + "username": "testuser", + "email": "test@example.com", + "password": "test password", + "confirm_password": "test password", + } + request = rf.post(reverse("accounts.signup"), data) + response = view(request) + + assert response.status_code == 303 + assert "Location" in response + assert response["Location"] == resolve_url(settings.LOGIN_REDIRECT_URL) + + assert creator.call_count == 1 + assert creator.call_args == (tuple(), { + "username": "testuser", + "email": "test@example.com", + "password": "test password", + }) + + +@pytest.mark.parametrize(("data", "errors"), [ + ( + { + "email": "test@example.com", + "password": "test password", + "confirm_password": "test password", + }, + {"username": ["This field is required."]}, + ), +]) +def test_signup_invalid(data, errors, rf): + class TestSignupForm(SignupForm): + model = stub(api=stub(username_exists=lambda x: False)) + + creator = mock.Mock() + view = SignupView.as_view(creator=creator, form_class=TestSignupForm) + + request = rf.post(reverse("accounts.signup"), data) + response = view(request) + + assert response.status_code == 200 + assert response.context_data.keys() == set(["next", "form"]) + assert response.context_data["next"] is None + assert isinstance(response.context_data["form"], SignupForm) + assert response.context_data["form"].errors == errors + + +def test_signup_ensure_next(rf): + class TestSignupForm(SignupForm): + model = stub(api=stub(username_exists=lambda x: False)) + + view = SignupView.as_view( + creator=lambda *args, **kwargs: None, + form_class=TestSignupForm, + ) + + # Test that next is properly added to the context for a GET + request = rf.get(reverse("accounts.signup"), {"next": "/test/next/"}) + response = view(request) + assert response.context_data["next"] == "/test/next/" + + # Test that next is properly added to the context for an invalid POST + request = rf.post(reverse("accounts.signup"), {"next": "/test/next/"}) + response = view(request) + assert response.context_data["next"] == "/test/next/" + + # Test that when given valid data POST redirects to next + request = rf.post(reverse("accounts.signup"), { + "next": "/test/next/", + "username": "testuser", + "email": "test@example.com", + "password": "test password", + "confirm_password": "test password", + }) + response = view(request) + assert response["Location"] == "/test/next/" diff --git a/tests/unit/management/__init__.py b/tests/unit/management/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/unit/management/commands/__init__.py b/tests/unit/management/commands/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/management/commands/test_init.py b/tests/unit/management/commands/test_init.py similarity index 100% rename from tests/management/commands/test_init.py rename to tests/unit/management/commands/test_init.py diff --git a/tests/unit/test_adapters.py b/tests/unit/test_adapters.py new file mode 100644 index 000000000000..35c9b09247e7 --- /dev/null +++ b/tests/unit/test_adapters.py @@ -0,0 +1,28 @@ +import pytest + +from warehouse.adapters import BaseAdapter + + +def test_adapter_contributes(): + class Foo(object): + pass + + adapter = BaseAdapter() + adapter.contribute_to_class(Foo, "api") + + assert Foo.api is adapter + + +def test_adapter_not_instances(): + class Foo(object): + pass + + adapter = BaseAdapter() + adapter.contribute_to_class(Foo, "api") + + # Make sure this doesn't raise an exception + Foo.api + + # Make sure this does raise an exception + with pytest.raises(AttributeError): + Foo().api diff --git a/tests/test_context_processors.py b/tests/unit/test_context_processors.py similarity index 100% rename from tests/test_context_processors.py rename to tests/unit/test_context_processors.py diff --git a/tests/unit/utils/__init__.py b/tests/unit/utils/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/unit/utils/templatetags/__init__.py b/tests/unit/utils/templatetags/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/unit/utils/templatetags/test_form_utils.py b/tests/unit/utils/templatetags/test_form_utils.py new file mode 100644 index 000000000000..fc234f477016 --- /dev/null +++ b/tests/unit/utils/templatetags/test_form_utils.py @@ -0,0 +1,18 @@ +from unittest import mock + +from warehouse.utils.templatetags.form_utils import renderfield + + +def test_renderfield(): + # Setup a mock field object + field = mock.NonCallableMock() + widget = mock.Mock(return_value="Rendered Widget!") + field.as_widget = widget + + # Attempt to render the field + rendered = renderfield(field, **{"class": "my-class"}) + + # Verify results + assert rendered == "Rendered Widget!" + assert widget.call_count == 1 + assert widget.call_args == (tuple(), {"attrs": {"class": "my-class"}}) diff --git a/tests/unit/utils/test_mail.py b/tests/unit/utils/test_mail.py new file mode 100644 index 000000000000..1df893251bf4 --- /dev/null +++ b/tests/unit/utils/test_mail.py @@ -0,0 +1,33 @@ +import pytest + +from unittest import mock + +from warehouse.utils.mail import send_mail + + +@pytest.mark.parametrize( + ("recipients", "subject", "message", "from_address"), + [ + (["foo@example.com"], "Testing", "My Message", None), + (["foo@example.com"], "Testing", "My Message", "no-reply@example.com"), + ], +) +def test_send_mail(recipients, subject, message, from_address): + # Set up our FakeClass + instance = mock.NonCallableMock() + instance.send = mock.Mock() + mock_message = mock.Mock(return_value=instance) + + # Try to send a mail message + send_mail(recipients, subject, message, from_address, + message_class=mock_message, + ) + + assert mock_message.call_count == 1 + assert mock_message.call_args == ( + (subject, message, from_address, recipients), + {}, + ) + + assert instance.send.call_count == 1 + assert instance.send.call_args == (tuple(), {}) diff --git a/warehouse/accounts/adapters.py b/warehouse/accounts/adapters.py new file mode 100644 index 000000000000..a67932d31f29 --- /dev/null +++ b/warehouse/accounts/adapters.py @@ -0,0 +1,72 @@ +import collections + +from django.utils import timezone + +from warehouse.adapters import BaseAdapter + + +User = collections.namedtuple("User", ["username"]) +Email = collections.namedtuple("Email", + ["user", "email", "primary", "verified"], + ) + + +class UserAdapter(BaseAdapter): + + def _serialize(self, db_user): + return User( + username=db_user.username, + ) + + def create(self, username, password=None): + if not username: + raise ValueError("The given username must be set") + + # Create the user in the Database + now = timezone.now() + db_user = self.model( + username=username, + is_staff=False, + is_active=True, + is_superuser=False, + last_login=now, + date_joined=now, + ) + db_user.set_password(password) + db_user.save() + + # Serialize the db user + return self._serialize(db_user) + + def username_exists(self, username): + return self.model.objects.filter(username=username).exists() + + +class EmailAdapter(BaseAdapter): + + def __init__(self, *args, **kwargs): + self.User = kwargs.pop("user") + super(EmailAdapter, self).__init__(*args, **kwargs) + + def _serialize(self, db_email): + return Email( + user=db_email.user.username, + email=db_email.email, + primary=db_email.primary, + verified=db_email.verified, + ) + + def create(self, username, address, primary=False, verified=False): + # Fetch the user that we need + user = self.User.objects.get(username=username) + + # Create the email in the database + db_email = self.model( + user=user, + email=address, + primary=primary, + verified=verified, + ) + db_email.save() + + return self._serialize(db_email) diff --git a/warehouse/accounts/forms.py b/warehouse/accounts/forms.py index 4dbed741da27..df9f05982644 100644 --- a/warehouse/accounts/forms.py +++ b/warehouse/accounts/forms.py @@ -7,6 +7,47 @@ from warehouse.accounts.models import User +class SignupForm(forms.Form): + + model = User + + username = forms.RegexField( + label=_("Username"), + max_length=50, + regex=accounts.VALID_USERNAME_REGEX, + help_text=accounts.VALID_USERNAME_DESC, + error_messages={"invalid": accounts.INVALID_USERNAME_MSG}, + ) + + email = forms.EmailField(label=_("Email"), max_length=254) + + password = forms.CharField( + label=_("Password"), + widget=forms.PasswordInput, + min_length=8, + ) + + confirm_password = forms.CharField( + label=_("Confirm Password"), + widget=forms.PasswordInput, + ) + + def clean_username(self): + # Ensure that this username is not already taken + if self.model.api.username_exists(self.cleaned_data["username"]): + raise forms.ValidationError(_("Username is already taken")) + return self.cleaned_data["username"] + + def clean_confirm_password(self): + # Ensure that the confirm_password field matches the password field + password = self.cleaned_data.get("password", "") + confirm = self.cleaned_data.get("confirm_password", "") + + if password != confirm: + raise forms.ValidationError(_("Passwords do not match")) + return confirm + + class UserChangeForm(forms.ModelForm): username = forms.RegexField( diff --git a/warehouse/accounts/models.py b/warehouse/accounts/models.py index bb270691034d..ff1966af6dfe 100644 --- a/warehouse/accounts/models.py +++ b/warehouse/accounts/models.py @@ -12,6 +12,7 @@ ) from warehouse import accounts +from warehouse.accounts import adapters class UserManager(BaseUserManager): @@ -80,6 +81,7 @@ class User(AbstractBaseUser, PermissionsMixin): date_joined = models.DateTimeField(_("date joined"), default=timezone.now) objects = UserManager() + api = adapters.UserAdapter() @property def email(self): @@ -109,6 +111,8 @@ class Email(models.Model): primary = models.BooleanField(_("primary"), default=False) verified = models.BooleanField(_("verified"), default=False) + api = adapters.EmailAdapter(user=User) + class Meta: verbose_name = _("email") verbose_name_plural = _("emails") diff --git a/warehouse/accounts/regards.py b/warehouse/accounts/regards.py new file mode 100644 index 000000000000..660214355002 --- /dev/null +++ b/warehouse/accounts/regards.py @@ -0,0 +1,43 @@ +from django.conf import settings +from django.template.loader import render_to_string + +from warehouse.accounts.models import Email, User +from warehouse.utils.mail import send_mail + + +class UserCreator(object): + + user_creator = User.api.create + email_creator = Email.api.create + mailer = send_mail + + def __init__(self, user_creator=None, email_creator=None, mailer=None): + if user_creator is not None: + self.user_creator = user_creator + + if email_creator is not None: + self.email_creator = email_creator + + if mailer is not None: + self.mailer = mailer + + def __call__(self, username, email, password): + # Create the User in the Database + user = self.user_creator(username, password) + + # Associate the Email address with the User + user_email = self.email_creator(user.username, email) + + # Send an Email to the User + subject = render_to_string("accounts/emails/welcome_subject.txt", { + "user": user, + "SITE_NAME": settings.SITE_NAME, + }).strip() + body = render_to_string("accounts/emails/welcome_body.txt", { + "user": user, + "SITE_NAME": settings.SITE_NAME, + }).strip() + send_mail([user_email.email], subject, body) + + # Return the User + return user diff --git a/warehouse/accounts/urls.py b/warehouse/accounts/urls.py new file mode 100644 index 000000000000..0f36cd8d5ad7 --- /dev/null +++ b/warehouse/accounts/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import patterns, url + +from warehouse.accounts import views + +urlpatterns = patterns("", + url(r"^signup/$", views.signup, name="accounts.signup"), +) diff --git a/warehouse/accounts/views.py b/warehouse/accounts/views.py new file mode 100644 index 000000000000..e9cec759c8b7 --- /dev/null +++ b/warehouse/accounts/views.py @@ -0,0 +1,43 @@ +from django.conf import settings +from django.http import HttpResponseRedirect +from django.shortcuts import resolve_url +from django.views.generic import View +from django.views.generic.base import TemplateResponseMixin + +from warehouse.accounts.forms import SignupForm +from warehouse.accounts.regards import UserCreator + + +class SignupView(TemplateResponseMixin, View): + + creator = UserCreator() + form_class = SignupForm + template_name = "accounts/signup.html" + + def post(self, request): + form = self.form_class(request.POST) + next = request.REQUEST.get("next", None) + + if form.is_valid(): + # Create User + self.creator( + username=form.cleaned_data["username"], + email=form.cleaned_data["email"], + password=form.cleaned_data["password"], + ) + + # Redirect to the next page + if next is None: + next = resolve_url(settings.LOGIN_REDIRECT_URL) + + return HttpResponseRedirect(next, status=303) + + return self.render_to_response(dict(form=form, next=next)) + + def get(self, request): + form = self.form_class() + next = request.REQUEST.get("next", None) + + return self.render_to_response(dict(form=form, next=next)) + +signup = SignupView.as_view() diff --git a/warehouse/adapters.py b/warehouse/adapters.py new file mode 100644 index 000000000000..baef5e9a7075 --- /dev/null +++ b/warehouse/adapters.py @@ -0,0 +1,19 @@ +class BaseAdapter(object): + + def __init__(self, *args, **kwargs): + super(BaseAdapter, self).__init__(*args, **kwargs) + self.model = None + + def __get__(self, instance, type=None): + if instance is not None: + raise AttributeError( + "Manager isn't accessible via %s instances" % type.__name__) + return self + + def contribute_to_class(self, model, name): + # TODO: Use weakref because of possible memory leak / circular + # reference. + self.model = model + + # Add ourselves to the Model class + setattr(model, name, self) diff --git a/warehouse/conf.py b/warehouse/conf.py index 1eed90555a81..6878d0e06373 100644 --- a/warehouse/conf.py +++ b/warehouse/conf.py @@ -17,6 +17,7 @@ class Settings(BaseSettings): # Warehouse Apps "warehouse", "warehouse.accounts", + "warehouse.utils", ) AUTH_USER_MODEL = "accounts.User" diff --git a/warehouse/static/warehouse/css/style.css b/warehouse/static/warehouse/css/style.css index e69de29bb2d1..9de06d151f74 100644 --- a/warehouse/static/warehouse/css/style.css +++ b/warehouse/static/warehouse/css/style.css @@ -0,0 +1,3 @@ +#wrap > .container { padding-top: 60px; } + +#footer .credit { margin: 20px 0; } diff --git a/warehouse/templates/accounts/_signup_sidebar.html b/warehouse/templates/accounts/_signup_sidebar.html new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/warehouse/templates/accounts/emails/welcome_body.txt b/warehouse/templates/accounts/emails/welcome_body.txt new file mode 100644 index 000000000000..4039178b44b6 --- /dev/null +++ b/warehouse/templates/accounts/emails/welcome_body.txt @@ -0,0 +1 @@ +Welcome {{ user.username }}! diff --git a/warehouse/templates/accounts/emails/welcome_subject.txt b/warehouse/templates/accounts/emails/welcome_subject.txt new file mode 100644 index 000000000000..93c27adb0ab3 --- /dev/null +++ b/warehouse/templates/accounts/emails/welcome_subject.txt @@ -0,0 +1 @@ +Welcome to {{ SITE_NAME }}! diff --git a/warehouse/templates/accounts/signup.html b/warehouse/templates/accounts/signup.html new file mode 100644 index 000000000000..2abc272d306e --- /dev/null +++ b/warehouse/templates/accounts/signup.html @@ -0,0 +1,82 @@ +{% extends "site_base.html" %} + +{% load i18n %} +{% load form_utils %} + +{% block head_title %}{% trans "Sign up" %}{% endblock %} + +{% block body_id %}sign-up{% endblock %} + +{% block content %} + +
+
+
+ {% csrf_token %} + + {% if next %} +
+ {% endif %} + +
+
+ {% if form.username.errors %} + {% for error in form.username.errors %} + {{ error }} + {% endfor %} + {% endif %} + + {% trans "Username" as username %} + {% renderfield form.username placeholder=username class="input-xlarge" %} +
+
+ +
+
+ {% if form.email.errors %} + {% for error in form.email.errors %} + {{ error }} + {% endfor %} + {% endif %} + + {% trans "Email" as email %} + {% renderfield form.email placeholder=email class="input-xlarge" %} +
+
+ +
+
+ {% if form.password.errors %} + {% for error in form.password.errors %} + {{ error }} + {% endfor %} + {% endif %} + + {% trans "Password" as password %} + {% renderfield form.password placeholder=password class="input-xlarge" %} +
+
+ +
+
+ {% if form.confirm_password.errors %} + {% for error in form.confirm_password.errors %} + {{ error }} + {% endfor %} + {% endif %} + + {% trans "Confirm Password" as confirm_password %} + {% renderfield form.confirm_password placeholder=confirm_password class="input-xlarge" %} +
+
+ + +
+
+
+ {% include "accounts/_signup_sidebar.html" %} +
+
+{% endblock %} diff --git a/warehouse/urls.py b/warehouse/urls.py index db35259c48f0..9808ef71ec80 100644 --- a/warehouse/urls.py +++ b/warehouse/urls.py @@ -4,5 +4,6 @@ admin.autodiscover() urlpatterns = patterns("", + url(r"^account/", include("warehouse.accounts.urls")), url(r"^admin/", include(admin.site.urls)), ) diff --git a/warehouse/utils/__init__.py b/warehouse/utils/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/warehouse/utils/mail.py b/warehouse/utils/mail.py new file mode 100644 index 000000000000..234c1dd26098 --- /dev/null +++ b/warehouse/utils/mail.py @@ -0,0 +1,6 @@ +from django.core.mail import EmailMessage + + +def send_mail(recipients, subject, message, from_address=None, + message_class=EmailMessage): + return message_class(subject, message, from_address, recipients).send() diff --git a/warehouse/utils/templatetags/__init__.py b/warehouse/utils/templatetags/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/warehouse/utils/templatetags/form_utils.py b/warehouse/utils/templatetags/form_utils.py new file mode 100644 index 000000000000..924cb9b6c9f7 --- /dev/null +++ b/warehouse/utils/templatetags/form_utils.py @@ -0,0 +1,9 @@ +from django import template + + +register = template.Library() + + +@register.simple_tag +def renderfield(field, **attrs): + return field.as_widget(attrs=attrs)