From d5ee8415ede81e8d1ac78a12ec01fbc8d45004b7 Mon Sep 17 00:00:00 2001 From: arvid-e Date: Wed, 23 Jul 2025 09:43:13 +0900 Subject: [PATCH 1/6] Add signup path and template --- templates/registration/signup.html | 16 ++++++++++++++++ twittercopy/urls.py | 5 +++++ users/forms.py | 8 ++++++++ users/urls.py | 1 + users/views.py | 9 +++++++++ 5 files changed, 39 insertions(+) create mode 100644 templates/registration/signup.html create mode 100644 users/forms.py diff --git a/templates/registration/signup.html b/templates/registration/signup.html new file mode 100644 index 0000000..c17ca00 --- /dev/null +++ b/templates/registration/signup.html @@ -0,0 +1,16 @@ + + + + + + Login + + +

Signup

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ + \ No newline at end of file diff --git a/twittercopy/urls.py b/twittercopy/urls.py index 2761efd..ed1cad2 100644 --- a/twittercopy/urls.py +++ b/twittercopy/urls.py @@ -1,10 +1,15 @@ from django.contrib import admin from django.urls import path, include from django.views.generic.base import TemplateView +from users.views import SignUpView + urlpatterns = [ path('admin/', admin.site.urls), + path('accounts/', include('django.contrib.auth.urls')), + path('accounts/signup/', SignUpView.as_view(), name='signup'), + path('users/', include('users.urls')), path('tweets/', include('tweets.urls')), path('followers/', include('followers.urls')), diff --git a/users/forms.py b/users/forms.py new file mode 100644 index 0000000..c004769 --- /dev/null +++ b/users/forms.py @@ -0,0 +1,8 @@ +from django.contrib.auth.forms import UserCreationForm +from .models import CustomUser + + +class CustomUserCreationForm(UserCreationForm): + class Meta(UserCreationForm.Meta): + model = CustomUser + fields = UserCreationForm.Meta.fields diff --git a/users/urls.py b/users/urls.py index 2a9865b..1e0f092 100644 --- a/users/urls.py +++ b/users/urls.py @@ -2,6 +2,7 @@ from django.urls import path from . import views + urlpatterns = [ path('profile//', views.profile_view, name='profile'), ] diff --git a/users/views.py b/users/views.py index d7c7fde..35e93ba 100644 --- a/users/views.py +++ b/users/views.py @@ -1,8 +1,17 @@ from django.shortcuts import render, get_object_or_404 from .models import CustomUser +from django.urls import reverse_lazy +from django.views import generic +from .forms import CustomUserCreationForm def profile_view(request, username): user = get_object_or_404(CustomUser, username=username) context = {'user': user} return render(request, 'users/profile.html', context) + + +class SignUpView(generic.CreateView): + form_class = CustomUserCreationForm + success_url = reverse_lazy('login') + template_name = 'registration/signup.html' From 2428d4c383e26860c31b2a0ace164b9b47b572dc Mon Sep 17 00:00:00 2001 From: arvid-e Date: Wed, 23 Jul 2025 10:05:23 +0900 Subject: [PATCH 2/6] Add styles to signup and login pages --- static/css/styles.css | 130 +++++++++++++++++++++++++++++ templates/home.html | 1 + templates/registration/login.html | 21 +++-- templates/registration/signup.html | 20 +++-- twittercopy/settings.py | 6 -- 5 files changed, 157 insertions(+), 21 deletions(-) create mode 100644 static/css/styles.css diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..e093be8 --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,130 @@ + +/* Basic Reset & Font */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Inter', sans-serif; + background-color: #f0f2f5; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + color: #1a202c; + padding: 20px; +} + +/* Main Container for Login Form */ +.login-container { + background-color: #ffffff; + padding: 40px; + border-radius: 16px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 400px; + text-align: center; +} + +/* Heading */ +.login-container h2 { + font-size: 2.25rem; + color: #1da1f2; + margin-bottom: 30px; + font-weight: 700; +} + +/* Form Styling */ +.login-container form { + display: flex; + flex-direction: column; + gap: 15px; +} + +/* Input Fields */ +.login-container input[type="text"], +.login-container input[type="password"], +.login-container input[type="email"] { + width: 100%; + padding: 14px 16px; + border: 1px solid #ccd6dd; + border-radius: 8px; + font-size: 1rem; + color: #1a202c; + outline: none; + transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out; +} + +.login-container input[type="text"]:focus, +.login-container input[type="password"]:focus, +.login-container input[type="email"]:focus { + border-color: #1da1f2; + box-shadow: 0 0 0 3px rgba(29, 161, 242, 0.2); +} + +/* Paragraphs for form fields (Django's form.as_p renders labels and inputs in p tags) */ +.login-container form p { + margin-bottom: 0; + text-align: left; +} + +.login-container form p label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: #4a5568; + font-size: 0.9rem; +} + +/* Error messages for form fields */ +.login-container form ul.errorlist { + list-style: none; + padding: 0; + margin-top: 5px; + color: #e53e3e; + font-size: 0.875rem; + text-align: left; +} + +/* Submit Button */ +.login-container button[type="submit"] { + background-color: #1da1f2; + color: #ffffff; + padding: 14px 20px; + border: none; + border-radius: 9999px; + font-size: 1.1rem; + font-weight: 700; + cursor: pointer; + transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out; + margin-top: 15px; +} + +.login-container button[type="submit"]:hover { + background-color: #0c85d0; + transform: translateY(-1px); +} + +.login-container button[type="submit"]:active { + transform: translateY(0); /* Reset on click */ +} + +.login-container p.link-text { + margin-top: 25px; + font-size: 0.95rem; + color: #4a5568; +} + +.login-container p.link-text a { + color: #1da1f2; + text-decoration: none; + font-weight: 600; + transition: text-decoration 0.2s ease-in-out; +} + +.login-container p.link-text a:hover { + text-decoration: underline; +} + diff --git a/templates/home.html b/templates/home.html index 07cdc3a..1114608 100644 --- a/templates/home.html +++ b/templates/home.html @@ -3,6 +3,7 @@ + Twitter Clone Home diff --git a/templates/registration/login.html b/templates/registration/login.html index 9788802..eede311 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -1,17 +1,22 @@ +{% load static %} - Login + Sign Up + + -

Login

-
- {% csrf_token %} - {{ form.as_p }} - -
-

Don't have an account? Register here (add registration URL later)

+ \ No newline at end of file diff --git a/templates/registration/signup.html b/templates/registration/signup.html index c17ca00..12b398d 100644 --- a/templates/registration/signup.html +++ b/templates/registration/signup.html @@ -1,16 +1,22 @@ +{% load static %} - Login + Sign Up + + -

Signup

-
- {% csrf_token %} - {{ form.as_p }} - -
+ \ No newline at end of file diff --git a/twittercopy/settings.py b/twittercopy/settings.py index 8a4b9b1..49fdef7 100644 --- a/twittercopy/settings.py +++ b/twittercopy/settings.py @@ -90,15 +90,9 @@ # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, From 881338099b6887c3443e1afe96f3b375abd08ee0 Mon Sep 17 00:00:00 2001 From: arvid-e Date: Tue, 29 Jul 2025 09:21:05 +0900 Subject: [PATCH 3/6] Fix home screen styles --- static/css/styles.css | 165 ++++++++++++++++++++++++++++++++++++------ templates/home.html | 43 +++++++---- 2 files changed, 171 insertions(+), 37 deletions(-) diff --git a/static/css/styles.css b/static/css/styles.css index e093be8..a364395 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -1,5 +1,3 @@ - -/* Basic Reset & Font */ *, *::before, *::after { box-sizing: border-box; margin: 0; @@ -7,40 +5,40 @@ } body { - font-family: 'Inter', sans-serif; - background-color: #f0f2f5; + font-family: 'Inter', sans-serif; + background-color: #f0f2f5; display: flex; justify-content: center; align-items: center; - min-height: 100vh; - color: #1a202c; - padding: 20px; + min-height: 100vh; + color: #1a202c; + padding: 20px; } /* Main Container for Login Form */ .login-container { - background-color: #ffffff; + background-color: #ffffff; padding: 40px; - border-radius: 16px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-radius: 16px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); width: 100%; - max-width: 400px; + max-width: 400px; text-align: center; } /* Heading */ .login-container h2 { font-size: 2.25rem; - color: #1da1f2; + color: #1da1f2; margin-bottom: 30px; - font-weight: 700; + font-weight: 700; } /* Form Styling */ .login-container form { display: flex; flex-direction: column; - gap: 15px; + gap: 15px; } /* Input Fields */ @@ -60,21 +58,21 @@ body { .login-container input[type="text"]:focus, .login-container input[type="password"]:focus, .login-container input[type="email"]:focus { - border-color: #1da1f2; - box-shadow: 0 0 0 3px rgba(29, 161, 242, 0.2); + border-color: #1da1f2; + box-shadow: 0 0 0 3px rgba(29, 161, 242, 0.2); } -/* Paragraphs for form fields (Django's form.as_p renders labels and inputs in p tags) */ +/* Paragraphs for form fields */ .login-container form p { - margin-bottom: 0; - text-align: left; + margin-bottom: 0; + text-align: left; } .login-container form p label { display: block; margin-bottom: 8px; font-weight: 600; - color: #4a5568; + color: #4a5568; font-size: 0.9rem; } @@ -91,10 +89,10 @@ body { /* Submit Button */ .login-container button[type="submit"] { background-color: #1da1f2; - color: #ffffff; + color: #ffffff; padding: 14px 20px; border: none; - border-radius: 9999px; + border-radius: 9999px; font-size: 1.1rem; font-weight: 700; cursor: pointer; @@ -104,11 +102,11 @@ body { .login-container button[type="submit"]:hover { background-color: #0c85d0; - transform: translateY(-1px); + transform: translateY(-1px); } .login-container button[type="submit"]:active { - transform: translateY(0); /* Reset on click */ + transform: translateY(0); } .login-container p.link-text { @@ -128,3 +126,122 @@ body { text-decoration: underline; } +/* --- Home Screen Specific Styles --- */ + +/* Main Container for Home Screen Content */ +.home-container { + background-color: #ffffff; + padding: 40px; + border-radius: 16px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 600px; + text-align: center; + margin: auto; +} + +/* Main Heading (Twitter clone) */ +.home-container h1 { + font-size: 2.5rem; + color: #1da1f2; + margin-bottom: 20px; + font-weight: 700; +} + +/* Paragraphs (e.g., "Hello, {{ user.username }}!", "What would you like to do today?") */ +.home-container p { + font-size: 1.1rem; + line-height: 1.6; + margin-bottom: 15px; + color: #4a5568; +} + +/* Span for username highlighting */ +.home-container p span { + color: #1da1f2; + font-weight: 700; +} + +/* Container for authenticated user links (View Tweets, Create Tweet, My Profile, Logout) */ +.home-container .app-links { + margin-top: 30px; + display: flex; + flex-direction: column; + gap: 15px; + align-items: center; +} + +/* Container for unauthenticated user links (Log In, Sign Up) */ +.home-container .auth-links { + margin-top: 30px; + display: flex; + flex-direction: column; + gap: 15px; + align-items: center; +} + +/* Styling for all links within app-links and auth-links */ +.home-container .app-links a, +.home-container .auth-links a { + display: inline-block; + background-color: #1da1f2; + color: #ffffff; + padding: 12px 25px; + border: none; + border-radius: 9999px; + text-decoration: none; + font-weight: 600; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out; + min-width: 180px; + text-align: center; +} + +.home-container .app-links a:hover, +.home-container .auth-links a:hover { + background-color: #0c85d0; + transform: translateY(-1px); +} + +/* Styling for the "Or explore as a guest." text */ +.home-container p.small-link { + font-size: 0.9rem; + color: #4a5568; + margin-top: 20px; +} + +.home-container p.small-link a { + color: #1da1f2; + text-decoration: none; + font-weight: 600; +} + +.home-container p.small-link a:hover { + text-decoration: underline; +} + +/* Responsive Adjustments for Home Screen */ +@media (max-width: 600px) { + body { + padding: 15px; + } + .home-container { + padding: 30px 25px; + border-radius: 12px; + max-width: 100%; + } + .home-container h1 { + font-size: 2rem; + } + .home-container p { + font-size: 1rem; + } + .home-container .app-links a, + .home-container .auth-links a { + padding: 10px 20px; + font-size: 0.9rem; + min-width: unset; + width: 100%; + } +} diff --git a/templates/home.html b/templates/home.html index 1114608..bc9ef83 100644 --- a/templates/home.html +++ b/templates/home.html @@ -1,22 +1,39 @@ +{% load static %} - + {# Link to your styles.css #} + {# Google Fonts link #} Twitter Clone Home -

Twitter clone

- {% if user.is_authenticated %} -

Hello, {{ user.username }}!

-

View Tweets

-

Create New Tweet

-

My Profile

-

Logout

- {% else %} -

Login

-

Admin

- {% endif %} +
+

Twitter clone

+ + {% if user.is_authenticated %} +

Hello, {{ user.username }}!

+

What would you like to do today?

+ + + {% else %} +

Join the conversation or log in to see what's happening.

{# Added for context #} + + + {# Removed Admin link from here as it's not typically a user-facing link on a home screen #} + + {% endif %} +
- \ No newline at end of file + From a2a9d90205c74b847d2cb702e54044827ea74c2b Mon Sep 17 00:00:00 2001 From: arvid-e Date: Thu, 14 Aug 2025 13:57:26 +0900 Subject: [PATCH 4/6] Add three tests for the signup functionality --- templates/registration/login.html | 2 +- templates/registration/signup.html | 2 +- templates/tweets/tweet-list.html | 15 +++++++++ tweets/urls.py | 4 ++- tweets/views.py | 8 +++-- twittercopy/urls.py | 12 ++++--- users/forms.py | 5 ++- users/tests.py | 50 +++++++++++++++++++++++++++++- users/urls.py | 2 +- users/views.py | 9 +++++- 10 files changed, 94 insertions(+), 15 deletions(-) create mode 100644 templates/tweets/tweet-list.html diff --git a/templates/registration/login.html b/templates/registration/login.html index eede311..1e35e0e 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -16,7 +16,7 @@

Login

{# Changed heading #} {{ form.as_p }} - + \ No newline at end of file diff --git a/templates/registration/signup.html b/templates/registration/signup.html index 12b398d..c948bf6 100644 --- a/templates/registration/signup.html +++ b/templates/registration/signup.html @@ -16,7 +16,7 @@

Sign Up

{# Changed heading #} {{ form.as_p }} - + \ No newline at end of file diff --git a/templates/tweets/tweet-list.html b/templates/tweets/tweet-list.html new file mode 100644 index 0000000..9c79a98 --- /dev/null +++ b/templates/tweets/tweet-list.html @@ -0,0 +1,15 @@ +{% load static %} + + + + + + Sign Up + + + + +
+ + + \ No newline at end of file diff --git a/tweets/urls.py b/tweets/urls.py index 1613a63..7f0064a 100644 --- a/tweets/urls.py +++ b/tweets/urls.py @@ -1,8 +1,10 @@ from django.urls import path from . import views +app_name = 'tweets' + urlpatterns = [ - path('', views.tweet_list_view, name='tweet_list'), + path('', views.tweet_list_view, name='home'), path('create/', views.tweet_create_view, name='tweet_create'), path('/like/', views.tweet_like_view, name='tweet_like'), ] diff --git a/tweets/views.py b/tweets/views.py index 71a1d1d..cf9be66 100644 --- a/tweets/views.py +++ b/tweets/views.py @@ -1,4 +1,4 @@ -from django.shortcuts import render, redirect, get_object_or_404 +from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from .models import Tweet @@ -6,11 +6,13 @@ def tweet_list_view(request): tweets = Tweet.objects.all() context = {'tweets': tweets} - return render(request, 'tweets/tweet_list.html', context) + return render(request, 'tweets/tweet-list.html', context) + @login_required def tweet_create_view(request): - return render(request, 'tweets/tweet_create.html') + return render(request, 'tweets/tweet-create.html') + @login_required def tweet_like_view(request, pk): diff --git a/twittercopy/urls.py b/twittercopy/urls.py index ed1cad2..cdcbcab 100644 --- a/twittercopy/urls.py +++ b/twittercopy/urls.py @@ -1,17 +1,19 @@ from django.contrib import admin from django.urls import path, include from django.views.generic.base import TemplateView -from users.views import SignUpView - +from users.views import SignUpView urlpatterns = [ path('admin/', admin.site.urls), - path('accounts/', include('django.contrib.auth.urls')), - path('accounts/signup/', SignUpView.as_view(), name='signup'), + path('account/', include(([ + path('signup/', SignUpView.as_view(), name='signup'), + path('', include('django.contrib.auth.urls')), + ], 'account'), namespace='account')), path('users/', include('users.urls')), - path('tweets/', include('tweets.urls')), + path('tweets/', include(('tweets.urls', 'tweets'), namespace='tweets')), path('followers/', include('followers.urls')), + path('', TemplateView.as_view(template_name='home.html'), name='home') ] diff --git a/users/forms.py b/users/forms.py index c004769..1bff493 100644 --- a/users/forms.py +++ b/users/forms.py @@ -1,8 +1,11 @@ +from django import forms from django.contrib.auth.forms import UserCreationForm from .models import CustomUser class CustomUserCreationForm(UserCreationForm): + email = forms.EmailField(required=True) + class Meta(UserCreationForm.Meta): model = CustomUser - fields = UserCreationForm.Meta.fields + fields = UserCreationForm.Meta.fields + ('email',) diff --git a/users/tests.py b/users/tests.py index 7ce503c..6e4d274 100644 --- a/users/tests.py +++ b/users/tests.py @@ -1,3 +1,51 @@ +from django.contrib.auth import SESSION_KEY, get_user_model from django.test import TestCase +from django.urls import reverse -# Create your tests here. +User = get_user_model() + + +class TestSignupView(TestCase): + def setUp(self): + self.url = reverse("account:signup") + + def test_success_get(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "registration/signup.html") + + def test_success_post(self): + valid_data = { + "username": "testuser", + "email": "test@test.com", + "password1": "testpassword", + "password2": "testpassword", + } + response = self.client.post(self.url, valid_data) + + # 1の確認 = tweets/homeにリダイレクトすること + self.assertRedirects( + response, + reverse("tweets:home"), + status_code=302, + target_status_code=200, + ) + # 2の確認 = ユーザーが作成されること + self.assertTrue(User.objects.filter(username=valid_data["username"]).exists()) + # 3の確認 = ログイン状態になること + self.assertIn(SESSION_KEY, self.client.session) + + def test_failure_post_with_empty_username(self): + invalid_data = { + "username": "", + "email": "test@test.com", + "password1": "testpassword", + "password2": "testpassword", + } + response = self.client.post(self.url, invalid_data) + form = response.context["form"] + + self.assertEqual(response.status_code, 200) + self.assertFalse(User.objects.filter(username=invalid_data["username"]).exists()) + self.assertFalse(form.is_valid()) + self.assertIn("This field is required.", form.errors["username"]) diff --git a/users/urls.py b/users/urls.py index 1e0f092..6a1d49a 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,7 +1,7 @@ - from django.urls import path from . import views +app_name = 'users' urlpatterns = [ path('profile//', views.profile_view, name='profile'), diff --git a/users/views.py b/users/views.py index 35e93ba..6e1a5eb 100644 --- a/users/views.py +++ b/users/views.py @@ -1,3 +1,4 @@ +from django.contrib.auth import login from django.shortcuts import render, get_object_or_404 from .models import CustomUser from django.urls import reverse_lazy @@ -13,5 +14,11 @@ def profile_view(request, username): class SignUpView(generic.CreateView): form_class = CustomUserCreationForm - success_url = reverse_lazy('login') + success_url = reverse_lazy('tweets:home') template_name = 'registration/signup.html' + + def form_valid(self, form): + response = super().form_valid(form) + user = form.save() + login(self.request, user) + return response From 6e1dafdc88eaa641cb16a72ee3d7e09dcbb6eb5c Mon Sep 17 00:00:00 2001 From: arvid-e Date: Thu, 14 Aug 2025 14:23:40 +0900 Subject: [PATCH 5/6] Add three more tests to the signup functionality --- templates/home.html | 7 +++--- tweets/urls.py | 2 +- users/tests.py | 61 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/templates/home.html b/templates/home.html index bc9ef83..e278217 100644 --- a/templates/home.html +++ b/templates/home.html @@ -17,10 +17,9 @@

Twitter clone

What would you like to do today?

{% else %}

Join the conversation or log in to see what's happening.

{# Added for context #} diff --git a/tweets/urls.py b/tweets/urls.py index 7f0064a..c649eed 100644 --- a/tweets/urls.py +++ b/tweets/urls.py @@ -5,6 +5,6 @@ urlpatterns = [ path('', views.tweet_list_view, name='home'), - path('create/', views.tweet_create_view, name='tweet_create'), + path('create/', views.tweet_create_view, name='create'), path('/like/', views.tweet_like_view, name='tweet_like'), ] diff --git a/users/tests.py b/users/tests.py index 6e4d274..f8a4c97 100644 --- a/users/tests.py +++ b/users/tests.py @@ -8,6 +8,7 @@ class TestSignupView(TestCase): def setUp(self): self.url = reverse("account:signup") + self.initial_user_count = User.objects.count() def test_success_get(self): response = self.client.get(self.url) @@ -49,3 +50,63 @@ def test_failure_post_with_empty_username(self): self.assertFalse(User.objects.filter(username=invalid_data["username"]).exists()) self.assertFalse(form.is_valid()) self.assertIn("This field is required.", form.errors["username"]) + + def test_failure_post_with_empty_email(self): + invalid_data = { + "username": "tester", + "email": "", + "password1": "testpassword", + "password2": "testpassword", + } + + response = self.client.post(self.url, invalid_data) + + self.assertEqual(response.status_code, 200) + + form = response.context["form"] + + self.assertFalse(form.is_valid()) + self.assertEqual(User.objects.count(), self.initial_user_count) + self.assertIn("This field is required.", form.errors["email"]) + + def test_failure_post_with_empty_password(self): + invalid_data = { + "username": "tester", + "email": "tester@tester.com", + "password1": "", + "password2": "", + } + + response = self.client.post(self.url, invalid_data) + + self.assertEqual(response.status_code, 200) + + form = response.context["form"] + + self.assertFalse(form.is_valid()) + self.assertEqual(User.objects.count(), self.initial_user_count) + self.assertIn("This field is required.", form.errors["password1"]) + self.assertIn("This field is required.", form.errors["password2"]) + + def test_failure_post_with_empty_form(self): + invalid_data = { + "username": "", + "email": "", + "password1": "", + "password2": "", + } + + response = self.client.post(self.url, invalid_data) + + self.assertEqual(response.status_code, 200) + + form = response.context["form"] + + self.assertFalse(form.is_valid()) + self.assertEqual(User.objects.count(), self.initial_user_count) + + self.assertIn("This field is required.", form.errors["username"]) + self.assertIn("This field is required.", form.errors["email"]) + self.assertIn("This field is required.", form.errors["password1"]) + self.assertIn("This field is required.", form.errors["password2"]) + From 4f62cb9c99ae7e90b59bed9bf0b979f94dc503ea Mon Sep 17 00:00:00 2001 From: arvid-e Date: Fri, 29 Aug 2025 12:37:06 +0900 Subject: [PATCH 6/6] Add tests and format code --- .flake8 | 3 + finalvenv/bin/black | 10 ++ finalvenv/bin/blackd | 10 ++ finalvenv/bin/django-admin | 2 + finalvenv/bin/flake8 | 10 ++ finalvenv/bin/isort | 10 ++ finalvenv/bin/isort-identify-imports | 10 ++ finalvenv/bin/pip | 2 + finalvenv/bin/pip3 | 2 + finalvenv/bin/pip3.12 | 2 + finalvenv/bin/pycodestyle | 10 ++ finalvenv/bin/pyflakes | 10 ++ finalvenv/bin/sqlformat | 2 + followers/admin.py | 1 + followers/apps.py | 4 +- followers/migrations/0001_initial.py | 17 +++- followers/migrations/0002_initial.py | 26 +++-- followers/models.py | 17 +++- followers/tests.py | 3 - followers/urls.py | 7 +- followers/views.py | 9 +- manage.py | 4 +- pyproject.toml | 2 + templates/home.html | 4 +- tweets/admin.py | 1 + tweets/apps.py | 4 +- tweets/migrations/0001_initial.py | 21 ++-- tweets/migrations/0002_initial.py | 22 ++-- tweets/models.py | 19 +++- tweets/tests.py | 3 - tweets/urls.py | 9 +- tweets/views.py | 11 +- twittercopy/asgi.py | 2 +- twittercopy/settings.py | 99 ++++++++++-------- twittercopy/urls.py | 33 +++--- twittercopy/wsgi.py | 2 +- users/admin.py | 1 + users/apps.py | 4 +- users/forms.py | 3 +- users/migrations/0001_initial.py | 137 +++++++++++++++++++++---- users/tests.py | 144 ++++++++++++++++++++++++++- users/urls.py | 5 +- users/views.py | 13 +-- 43 files changed, 550 insertions(+), 160 deletions(-) create mode 100644 .flake8 create mode 100755 finalvenv/bin/black create mode 100755 finalvenv/bin/blackd create mode 100755 finalvenv/bin/flake8 create mode 100755 finalvenv/bin/isort create mode 100755 finalvenv/bin/isort-identify-imports create mode 100755 finalvenv/bin/pycodestyle create mode 100755 finalvenv/bin/pyflakes create mode 100644 pyproject.toml diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..9e6dc3d --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +exclude = .git,__pycache__,venv,finalvenv, venv,migrations +max-line-length = 79 \ No newline at end of file diff --git a/finalvenv/bin/black b/finalvenv/bin/black new file mode 100755 index 0000000..8dce75b --- /dev/null +++ b/finalvenv/bin/black @@ -0,0 +1,10 @@ +#!/home/pasokon/Projects/backend-final-assignment-arvid-edvinsson/finalvenv/bin/python +# -*- coding: utf-8 -*- +import re +import sys + +from black import patched_main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(patched_main()) diff --git a/finalvenv/bin/blackd b/finalvenv/bin/blackd new file mode 100755 index 0000000..3e478c3 --- /dev/null +++ b/finalvenv/bin/blackd @@ -0,0 +1,10 @@ +#!/home/pasokon/Projects/backend-final-assignment-arvid-edvinsson/finalvenv/bin/python +# -*- coding: utf-8 -*- +import re +import sys + +from blackd import patched_main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(patched_main()) diff --git a/finalvenv/bin/django-admin b/finalvenv/bin/django-admin index fad4394..a1004aa 100755 --- a/finalvenv/bin/django-admin +++ b/finalvenv/bin/django-admin @@ -2,7 +2,9 @@ # -*- coding: utf-8 -*- import re import sys + from django.core.management import execute_from_command_line + if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) sys.exit(execute_from_command_line()) diff --git a/finalvenv/bin/flake8 b/finalvenv/bin/flake8 new file mode 100755 index 0000000..66d8556 --- /dev/null +++ b/finalvenv/bin/flake8 @@ -0,0 +1,10 @@ +#!/home/pasokon/Projects/backend-final-assignment-arvid-edvinsson/finalvenv/bin/python +# -*- coding: utf-8 -*- +import re +import sys + +from flake8.main.cli import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/finalvenv/bin/isort b/finalvenv/bin/isort new file mode 100755 index 0000000..80282b8 --- /dev/null +++ b/finalvenv/bin/isort @@ -0,0 +1,10 @@ +#!/home/pasokon/Projects/backend-final-assignment-arvid-edvinsson/finalvenv/bin/python +# -*- coding: utf-8 -*- +import re +import sys + +from isort.main import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/finalvenv/bin/isort-identify-imports b/finalvenv/bin/isort-identify-imports new file mode 100755 index 0000000..ed1df88 --- /dev/null +++ b/finalvenv/bin/isort-identify-imports @@ -0,0 +1,10 @@ +#!/home/pasokon/Projects/backend-final-assignment-arvid-edvinsson/finalvenv/bin/python +# -*- coding: utf-8 -*- +import re +import sys + +from isort.main import identify_imports_main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(identify_imports_main()) diff --git a/finalvenv/bin/pip b/finalvenv/bin/pip index 6da36b5..d6e945d 100755 --- a/finalvenv/bin/pip +++ b/finalvenv/bin/pip @@ -2,7 +2,9 @@ # -*- coding: utf-8 -*- import re import sys + from pip._internal.cli.main import main + if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) sys.exit(main()) diff --git a/finalvenv/bin/pip3 b/finalvenv/bin/pip3 index 6da36b5..d6e945d 100755 --- a/finalvenv/bin/pip3 +++ b/finalvenv/bin/pip3 @@ -2,7 +2,9 @@ # -*- coding: utf-8 -*- import re import sys + from pip._internal.cli.main import main + if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) sys.exit(main()) diff --git a/finalvenv/bin/pip3.12 b/finalvenv/bin/pip3.12 index 6da36b5..d6e945d 100755 --- a/finalvenv/bin/pip3.12 +++ b/finalvenv/bin/pip3.12 @@ -2,7 +2,9 @@ # -*- coding: utf-8 -*- import re import sys + from pip._internal.cli.main import main + if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) sys.exit(main()) diff --git a/finalvenv/bin/pycodestyle b/finalvenv/bin/pycodestyle new file mode 100755 index 0000000..2384a0e --- /dev/null +++ b/finalvenv/bin/pycodestyle @@ -0,0 +1,10 @@ +#!/home/pasokon/Projects/backend-final-assignment-arvid-edvinsson/finalvenv/bin/python +# -*- coding: utf-8 -*- +import re +import sys + +from pycodestyle import _main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(_main()) diff --git a/finalvenv/bin/pyflakes b/finalvenv/bin/pyflakes new file mode 100755 index 0000000..8dd292a --- /dev/null +++ b/finalvenv/bin/pyflakes @@ -0,0 +1,10 @@ +#!/home/pasokon/Projects/backend-final-assignment-arvid-edvinsson/finalvenv/bin/python +# -*- coding: utf-8 -*- +import re +import sys + +from pyflakes.api import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/finalvenv/bin/sqlformat b/finalvenv/bin/sqlformat index 984456e..d1d8609 100755 --- a/finalvenv/bin/sqlformat +++ b/finalvenv/bin/sqlformat @@ -2,7 +2,9 @@ # -*- coding: utf-8 -*- import re import sys + from sqlparse.__main__ import main + if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) sys.exit(main()) diff --git a/followers/admin.py b/followers/admin.py index 8721cd6..1d7cf23 100644 --- a/followers/admin.py +++ b/followers/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin + from .models import Follow admin.site.register(Follow) diff --git a/followers/apps.py b/followers/apps.py index 1c742d7..de59152 100644 --- a/followers/apps.py +++ b/followers/apps.py @@ -2,5 +2,5 @@ class FollowersConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'followers' + default_auto_field = "django.db.models.BigAutoField" + name = "followers" diff --git a/followers/migrations/0001_initial.py b/followers/migrations/0001_initial.py index cd814a3..551c7c6 100644 --- a/followers/migrations/0001_initial.py +++ b/followers/migrations/0001_initial.py @@ -7,15 +7,22 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Follow', + name="Follow", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), ], ), ] diff --git a/followers/migrations/0002_initial.py b/followers/migrations/0002_initial.py index d793654..341ccab 100644 --- a/followers/migrations/0002_initial.py +++ b/followers/migrations/0002_initial.py @@ -10,23 +10,31 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('followers', '0001_initial'), + ("followers", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AddField( - model_name='follow', - name='followed', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='followers', to=settings.AUTH_USER_MODEL), + model_name="follow", + name="followed", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="followers", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='follow', - name='follower', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following', to=settings.AUTH_USER_MODEL), + model_name="follow", + name="follower", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="following", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterUniqueTogether( - name='follow', - unique_together={('follower', 'followed')}, + name="follow", + unique_together={("follower", "followed")}, ), ] diff --git a/followers/models.py b/followers/models.py index a8455a2..b7c59c5 100644 --- a/followers/models.py +++ b/followers/models.py @@ -1,14 +1,23 @@ -from django.db import models from django.conf import settings +from django.db import models class Follow(models.Model): - follower = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='following') - followed = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='followers') + follower = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="following", + ) + + followed = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="followers", + ) created_at = models.DateTimeField(auto_now_add=True) class Meta: - unique_together = ('follower', 'followed') + unique_together = ("follower", "followed") def __str__(self): return f"{self.follower.username} follows {self.followed.username}" diff --git a/followers/tests.py b/followers/tests.py index 7ce503c..e69de29 100644 --- a/followers/tests.py +++ b/followers/tests.py @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/followers/urls.py b/followers/urls.py index b8c700a..4e70e89 100644 --- a/followers/urls.py +++ b/followers/urls.py @@ -1,7 +1,10 @@ from django.urls import path + from . import views urlpatterns = [ - path('follow//', views.follow_user, name='follow_user'), - path('unfollow//', views.unfollow_user, name='unfollow_user') + path("follow//", views.follow_user, name="follow_user"), + path( + "unfollow//", views.unfollow_user, name="unfollow_user" + ), ] diff --git a/followers/views.py b/followers/views.py index 1627c56..54397be 100644 --- a/followers/views.py +++ b/followers/views.py @@ -1,15 +1,12 @@ -from django.shortcuts import redirect, get_object_or_404 from django.contrib.auth.decorators import login_required -from django.contrib import messages -from users.models import CustomUser -from .models import Follow +from django.shortcuts import redirect @login_required def follow_user(request, username): - return redirect('home') + return redirect("home") @login_required def unfollow_user(request, username): - return redirect('home') \ No newline at end of file + return redirect("home") diff --git a/manage.py b/manage.py index f672733..ae32e4b 100755 --- a/manage.py +++ b/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'twittercopy.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "twittercopy.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9216134 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length = 79 \ No newline at end of file diff --git a/templates/home.html b/templates/home.html index e278217..ad5fbfd 100644 --- a/templates/home.html +++ b/templates/home.html @@ -25,8 +25,8 @@

Twitter clone

Join the conversation or log in to see what's happening.

{# Added for context #} {# Removed Admin link from here as it's not typically a user-facing link on a home screen #}