diff --git a/project/accounts/authentication.py b/project/accounts/authentication.py index 1219f8793..5d71efe3f 100644 --- a/project/accounts/authentication.py +++ b/project/accounts/authentication.py @@ -1,29 +1,17 @@ from django.conf import settings -from django.contrib.auth import authenticate, logout, login from django.contrib.auth import get_user_model from django.contrib.auth.tokens import PasswordResetTokenGenerator -from django.contrib.sites.shortcuts import get_current_site -from django.http import ( - JsonResponse, - HttpResponse, - HttpResponseServerError, - HttpResponseRedirect, - HttpResponseBadRequest, -) -from django.shortcuts import get_object_or_404 from django.template.response import TemplateResponse # TODO: move this out to views from django.utils.crypto import salted_hmac from django.utils.encoding import force_bytes, force_text from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode from django.utils.http import int_to_base36 -from django.views.decorators.debug import sensitive_post_parameters from django.template.loader import render_to_string from accounts.utils import send_email from accounts.models import Profile -from .forms import ProfileRegistrationForm, PasswordResetForm, RecoverUserForm -from core.custom_decorators import require_post_params +from .forms import PasswordResetForm, RecoverUserForm User = get_user_model() @@ -76,103 +64,6 @@ def send_activation_email(user, domain): ) -@sensitive_post_parameters("password") -@require_post_params(params=["username", "password"]) -def cw_login(request): - """ - USAGE: - This is used to authenticate the user and log them in. - - :returns (200, ok) (400, Inactive User) (400, Invalid username or password) - """ - - username = request.POST.get("username", "") - password = request.POST.get("password", "") - remember = request.POST.get("remember", "false") - - user = authenticate(username=username, password=password) - if user is not None: - if remember == "false": - request.session.set_expiry(0) - - login(request, user) - - if user.is_active: - - account = get_object_or_404(Profile, user=user) - request.session["login_user_firstname"] = account.first_name - request.session["login_user_image"] = account.profile_image_thumb_url - - return HttpResponse() - else: - response = {"message": "Inactive user", "error": "USER_INACTIVE"} - return JsonResponse(response, status=400) - else: - # Return an 'invalid login' error message. - response = {"message": "Invalid username or password", "error": "INVALID_LOGIN"} - return JsonResponse(response, status=400) - - -def cw_logout(request): - """Use this to logout the current user """ - - logout(request) - return HttpResponseRedirect("/") - - -@sensitive_post_parameters("password") -@require_post_params(params=["username", "password", "email"]) -def cw_register(request): - """ - USAGE: - This is used to register new users to civiwiki - - PROCESS: - - Gets new users username and password - - Sets the user to active - - Then creates a new user verification link and emails it to the new user - - Return: - (200, ok) (500, Internal Error) - """ - form = ProfileRegistrationForm(request.POST or None) - if request.method == "POST": - # Form Validation - if form.is_valid(): - username = form.clean_username() - password = form.clean_password() - email = form.clean_email() - - # Create a New Profile - try: - user = User.objects.create_user(username, email, password) - - account = Profile(user=user) - account.save() - - user.is_active = True - user.save() - - domain = get_current_site(request).domain - - send_activation_email(user, domain) - - login(request, user) - return HttpResponse() - - except Exception as e: - return HttpResponseServerError(reason=str(e)) - - else: - response = { - "success": False, - "errors": [error[0] for error in form.errors.values()], - } - return JsonResponse(response, status=400) - else: - return HttpResponseBadRequest(reason="POST Method Required") - - def activate_view(request, uidb64, token): """ This shows different views to the user when they are verifying diff --git a/project/accounts/forms.py b/project/accounts/forms.py index b3a879604..383441953 100644 --- a/project/accounts/forms.py +++ b/project/accounts/forms.py @@ -2,13 +2,11 @@ from django.core.files.images import get_image_dimensions from django import forms from django.contrib.auth.forms import ( - UserCreationForm, SetPasswordForm, PasswordResetForm as AuthRecoverUserForm, ) from django.forms.models import ModelForm from django.contrib.auth import get_user_model -from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.shortcuts import get_current_site from django.utils.encoding import force_bytes @@ -24,9 +22,9 @@ User = get_user_model() -class ProfileRegistrationForm(ModelForm): +class UserRegistrationForm(ModelForm): """ - This class is used to register new account in Civiwiki + This class is used to register a new user in Civiwiki Components: - Email - from registration form diff --git a/project/accounts/templates/accounts/login.html b/project/accounts/templates/accounts/login.html deleted file mode 100644 index 6cc1b08d3..000000000 --- a/project/accounts/templates/accounts/login.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - {% load static %} - {% include "base/links.html" %} - {% include "base/less_headers/login_less.html" %} - CiviWiki - - - - - - - -
- - - - - - diff --git a/project/accounts/tests.py b/project/accounts/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/project/accounts/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/project/accounts/tests/__init__.py b/project/accounts/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/project/accounts/tests/test_forms.py b/project/accounts/tests/test_forms.py new file mode 100644 index 000000000..77bc14bbc --- /dev/null +++ b/project/accounts/tests/test_forms.py @@ -0,0 +1,65 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase +from accounts.forms import UserRegistrationForm + + +class UserRegistrationFormTest(TestCase): + """A class to test user registration form""" + + def setUp(self) -> None: + self.data = { + "username": "testuser", + "email": "test@test.com", + "password": "password123" + } + + def test_user_creation_form_with_success(self): + """Whether form works as expected for the valid inputs""" + + form = UserRegistrationForm(self.data) + self.assertTrue(form.is_valid()) + self.assertEqual(form.errors, {}) + form.save() + self.assertTrue(get_user_model().objects.count(), 1) + + def test_form_is_unsuccessful_for_short_password(self): + """Whether a user does not have a short password""" + + self.data['password'] = "123" + form = UserRegistrationForm(self.data) + self.assertFalse(form.is_valid()) + self.assertNotEqual(form.errors, {}) + self.assertTrue(form.has_error('password')) + self.assertEqual(get_user_model().objects.count(), 0) + + def test_form_is_unsuccessful_for_only_digit_password(self): + """Whether a user does not have an only-digit password""" + + self.data['password'] = "12345678" + form = UserRegistrationForm(self.data) + self.assertFalse(form.is_valid()) + self.assertNotEqual(form.errors, {}) + self.assertTrue(form.has_error('password')) + self.assertEqual(get_user_model().objects.count(), 0) + + def test_form_is_unsuccessful_for_invalid_username(self): + """Whether a user does not have an invalid username""" + + self.data['username'] = "......." + form = UserRegistrationForm(self.data) + self.assertFalse(form.is_valid()) + self.assertNotEqual(form.errors, {}) + self.assertTrue(form.has_error('username')) + self.assertEqual(get_user_model().objects.count(), 0) + + def test_form_is_unsuccessful_for_existing_username_and_email(self): + """Whether a user does not have an existing username and email""" + + form = UserRegistrationForm(self.data) + form.save() + form = UserRegistrationForm(self.data) + self.assertFalse(form.is_valid()) + self.assertNotEqual(form.errors, {}) + self.assertTrue(form.has_error('username')) + self.assertTrue(form.has_error('email')) + self.assertEqual(get_user_model().objects.count(), 1) diff --git a/project/accounts/tests/test_models.py b/project/accounts/tests/test_models.py new file mode 100644 index 000000000..a6004682c --- /dev/null +++ b/project/accounts/tests/test_models.py @@ -0,0 +1,74 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase +from accounts.models import Profile + + +class BaseTestCase(TestCase): + """Base test class to set up test cases""" + + def setUp(self) -> None: + user = get_user_model().objects.create_user(username="testuser", email="test@test.com", password="password123") + self.test_profile = Profile.objects.create(user=user, first_name="Test", last_name="User", about_me="About Me") + + +class ProfileModelTests(BaseTestCase): + """A class to test Profile model""" + + def test_profile_creation(self): + """Whether the fields of created Profile instance is correct""" + + self.assertEqual(self.test_profile.first_name, "Test") + self.assertEqual(self.test_profile.last_name, "User") + self.assertEqual(self.test_profile.about_me, "About Me") + self.assertEqual(self.test_profile.full_name, "Test User") + + def test_profile_has_default_image_url(self): + """Whether a profile has a default image""" + + self.assertEqual(self.test_profile.profile_image_url, '/static/img/no_image_md.png') + + +class ProfileManagerTests(BaseTestCase): + """A class to test ProfileManager""" + + def test_profile_summarize_with_no_history_no_followers_no_following(self): + """Whether profile summarize is correct without history, followers, and followings""" + + data = { + "username": self.test_profile.user.username, + "first_name": self.test_profile.first_name, + "last_name": self.test_profile.last_name, + "about_me": self.test_profile.about_me, + "history": [], + "profile_image": self.test_profile.profile_image_url, + "followers": [], + "following": [], + } + self.assertEqual(Profile.objects.summarize(self.test_profile), data) + + def test_profile_chip_summarize(self): + """Whether profile chip summarize is correct""" + + data = { + "username": self.test_profile.user.username, + "first_name": self.test_profile.first_name, + "last_name": self.test_profile.last_name, + "profile_image": self.test_profile.profile_image_url, + } + self.assertEqual(Profile.objects.chip_summarize(self.test_profile), data) + + def test_profile_card_summarize(self): + """Whether profile card summarize is correct""" + + data = { + "id": self.test_profile.user.id, + "username": self.test_profile.user.username, + "first_name": self.test_profile.first_name, + "last_name": self.test_profile.last_name, + "about_me": self.test_profile.about_me, + "profile_image": self.test_profile.profile_image_url, + "follow_state": False, + "request_profile": self.test_profile.first_name, + } + self.assertEqual( + Profile.objects.card_summarize(self.test_profile, Profile.objects.get(user=self.test_profile.user)), data) diff --git a/project/accounts/tests/test_views.py b/project/accounts/tests/test_views.py new file mode 100644 index 000000000..81a273f42 --- /dev/null +++ b/project/accounts/tests/test_views.py @@ -0,0 +1,95 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse, resolve +from django.contrib.auth import views as auth_views +from accounts.models import Profile +from accounts.views import RegisterView + + +class BaseTestCase(TestCase): + """Base test class to set up test cases""" + + def setUp(self) -> None: + self.user = get_user_model().objects.create_user(username="newuser", + email="test@test.com", + password="password123") + self.profile = Profile.objects.create(user=self.user) + + +class LoginViewTests(BaseTestCase): + """A class to test login view""" + + def setUp(self) -> None: + super(LoginViewTests, self).setUp() + url = reverse('accounts_login') + self.response = self.client.get(url) + + def test_login_template(self): + """Whether login view uses the correct template""" + + self.assertEqual(self.response.status_code, 200) + self.assertTemplateUsed(self.response, 'accounts/register/login.html') + self.assertTemplateNotUsed(self.response, 'accounts/login.html') + self.assertContains(self.response, 'Log In') + self.assertNotContains(self.response, 'Wrong Content!') + + def test_login_url_matches_with_login_view(self): + """Whether login URL matches with the correct view""" + + view = resolve('/login/') + self.assertEqual(view.func.__name__, auth_views.LoginView.__name__) + + def test_the_user_with_the_correct_credentials_login(self): + """Whether a user with the correct credentials can login""" + + self.assertTrue(self.client.login(username="newuser", password="password123")) + + def test_login_view_redirects_on_success(self): + """Whether login view redirects to the base view after the successive try""" + + response = self.client.post(reverse('accounts_login'), + {'username': "newuser", + 'password': "password123"}) + self.assertRedirects(response, expected_url=reverse('base'), status_code=302, target_status_code=200) + + +class RegisterViewTests(TestCase): + """A class to test register view""" + + def setUp(self): + self.url = reverse('accounts_register') + + def test_register_template(self): + """Whether register view uses the correct template""" + + self.response = self.client.get(self.url) + self.assertEqual(self.response.status_code, 200) + self.assertTemplateUsed(self.response, 'accounts/register/register.html') + self.assertTemplateNotUsed(self.response, 'accounts/register.html') + self.assertContains(self.response, 'Register') + self.assertNotContains(self.response, 'Wrong Content!') + + def test_register_url_matches_with_register_view(self): + """Whether register URL matches with the correct view""" + + view = resolve('/register/') + self.assertEqual(view.func.__name__, RegisterView.__name__) + + def test_register_view_creates_a_user_successfully(self): + """Whether register view creates a new user with success""" + + user_count = get_user_model().objects.count() + self.client.post(reverse('accounts_register'), + {'username': "newuser", + "email": "newuser@email.com", + 'password': "password123"}) + self.assertEqual(get_user_model().objects.count(), user_count + 1) + + def test_register_view_redirects_on_success(self): + """Whether register view redirects to the base view after the successive try""" + + response = self.client.post(reverse('accounts_register'), + {'username': "newuser", + "email": "newuser@email.com", + 'password': "password123"}) + self.assertRedirects(response, expected_url=reverse('base'), status_code=302, target_status_code=200) diff --git a/project/accounts/urls.py b/project/accounts/urls.py index 726933769..32a955879 100644 --- a/project/accounts/urls.py +++ b/project/accounts/urls.py @@ -1,11 +1,17 @@ from django.conf.urls import url +from django.urls import path from django.contrib.auth import views as auth_views +from accounts.views import RegisterView from . import authentication urlpatterns = [ - url(r"^login", authentication.cw_login, name="login"), - url(r"^logout", authentication.cw_logout, name="logout"), - url(r"^register", authentication.cw_register, name="register"), + path( + 'login/', + auth_views.LoginView.as_view(template_name='accounts/register/login.html'), + name='accounts_login', + ), + path('logout/', auth_views.LogoutView.as_view(), name='accounts_logout'), + path('register/', RegisterView.as_view(), name='accounts_register'), url( r"^activate_account/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$", authentication.activate_view, diff --git a/project/accounts/views.py b/project/accounts/views.py index 18e6dd33b..09961dfbb 100644 --- a/project/accounts/views.py +++ b/project/accounts/views.py @@ -7,7 +7,7 @@ from django.conf import settings from django.views.generic.edit import FormView from django.contrib.auth import views as auth_views -from django.contrib.auth import authenticate, login +from django.contrib.auth import login from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.sites.shortcuts import get_current_site from django.utils.encoding import force_bytes @@ -16,12 +16,13 @@ from django.utils.http import urlsafe_base64_encode from django.urls import reverse_lazy from django.template.response import TemplateResponse +from django.contrib.auth import get_user_model from accounts.models import Profile from core.custom_decorators import login_required -from .forms import ProfileRegistrationForm, UpdateProfile -from .models import User +from accounts.forms import UserRegistrationForm, UpdateProfile + from .authentication import send_activation_email @@ -46,7 +47,7 @@ class RegisterView(FormView): """ template_name = "accounts/register/register.html" - form_class = ProfileRegistrationForm + form_class = UserRegistrationForm success_url = "/" def _create_user(self, form): @@ -54,13 +55,8 @@ def _create_user(self, form): password = form.cleaned_data["password"] email = form.cleaned_data["email"] - user = User.objects.create_user(username, email, password) - - account = Profile(user=user) - account.save() - - user.is_active = True - user.save() + user = get_user_model().objects.create_user(username, email, password) + Profile.objects.create(user=user) return user @@ -103,7 +99,7 @@ class PasswordResetCompleteView(auth_views.PasswordResetCompleteView): @login_required def settings_view(request): - account = request.user.account_set.first() + profile = request.user.profile_set.first() if request.method == "POST": instance = Profile.objects.get(user=request.user) form = UpdateProfile( @@ -118,9 +114,9 @@ def settings_view(request): initial={ "username": request.user.username, "email": request.user.email, - "first_name": account.first_name or None, - "last_name": account.last_name or None, - "about_me": account.about_me or None, + "first_name": profile.first_name or None, + "last_name": profile.last_name or None, + "about_me": profile.about_me or None, } ) return TemplateResponse(request, "accounts/utils/update_settings.html", {"form": form}) diff --git a/project/core/settings.py b/project/core/settings.py index e98d231b6..c2352ba90 100644 --- a/project/core/settings.py +++ b/project/core/settings.py @@ -55,7 +55,6 @@ CORS_ORIGIN_ALLOW_ALL = True ROOT_URLCONF = "core.urls" -LOGIN_URL = "/login" # SSL Setup if DJANGO_HOST != "LOCALHOST": @@ -172,11 +171,12 @@ # Custom User model AUTH_USER_MODEL = 'accounts.User' -APPEND_SLASH = False - DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +# Login Logout URLS +LOGIN_URL = "login/" LOGIN_REDIRECT_URL = '/' +LOGOUT_REDIRECT_URL = '/' AUTH_PASSWORD_VALIDATORS = [ { diff --git a/project/core/urls.py b/project/core/urls.py index acaee9ca5..ffef4b152 100644 --- a/project/core/urls.py +++ b/project/core/urls.py @@ -1,49 +1,36 @@ """civiwiki URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.8/topics/http/urls/ + https://docs.djangoproject.com/en/3.2/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') + 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf - 1. Add an import: from blog import urls as blog_urls - 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -import django.contrib.auth.views as auth_views -from django.conf.urls import include, url +from django.conf.urls import url +from django.urls import path, include from django.contrib import admin from django.conf import settings -from django.urls import path from django.views.static import serve from django.views.generic.base import RedirectView from api import urls as api -from accounts import urls as accounts_urls -from accounts.views import (RegisterView, PasswordResetView, PasswordResetDoneView, +from accounts.views import (PasswordResetView, PasswordResetDoneView, PasswordResetConfirmView, PasswordResetCompleteView, settings_view) from frontend_views import urls as frontend_views - urlpatterns = [ path("admin/", admin.site.urls), + path("", include('accounts.urls')), url(r"^api/", include(api)), - url(r"^auth/", include(accounts_urls)), - - # New accounts paths. These currently implement user registration/authentication in - # parallel to the current authentication. - path('accounts/register', RegisterView.as_view(), name='accounts_register'), - path( - 'accounts/login', - auth_views.LoginView.as_view(template_name='accounts/register/login.html'), - name='accounts_login', - ), - path( 'accounts/password_reset', PasswordResetView.as_view(), diff --git a/project/frontend_views/urls.py b/project/frontend_views/urls.py index 900ff5d8e..9b8319831 100644 --- a/project/frontend_views/urls.py +++ b/project/frontend_views/urls.py @@ -2,7 +2,6 @@ from . import views as v urlpatterns = [ - url(r"^login$", v.login_view, name="login"), url(r"^about$", v.about_view, name="about"), url(r"^support_us$", v.support_us_view, name="support us"), url(r"^howitworks$", v.how_it_works_view, name="how it works"), diff --git a/project/frontend_views/views.py b/project/frontend_views/views.py index c9fc73061..0d5aba503 100644 --- a/project/frontend_views/views.py +++ b/project/frontend_views/views.py @@ -182,14 +182,6 @@ def create_group(request): return TemplateResponse(request, "newgroup.html", {}) -def login_view(request): - if request.user.is_authenticated: - if request.user.is_active: - return HttpResponseRedirect("/") - - return TemplateResponse(request, "login.html", {}) - - def declaration(request): return TemplateResponse(request, "declaration.html", {}) diff --git a/project/setup.cfg b/project/setup.cfg index 42641fd6c..baee56619 100644 --- a/project/setup.cfg +++ b/project/setup.cfg @@ -1,4 +1,13 @@ [flake8] -ignore = E128,E265,E261,E126,E501,E302,E262,E127,E303,E226,E231,E201,E202,E121,E203,E123,W293,W391,E122,W292,F403,E401,E131,W503,E731,E266 +ignore = E128,E265,E261,E126,E501,E302,E262,E127,E226,E231,E201,E202,E121,E203,E123,W293,W391,E122,W292,F403,E401,E131,W503,E731,E266 max-line-length = 160 exclude = api/migrations, __init__.py + +[coverage:run] +omit = + */env/*, + */venv/*, + *tests*, + */migrations/*, + manage.py, + *__init__.py, \ No newline at end of file diff --git a/project/threads/templates/threads/partials/utils/global_nav.html b/project/threads/templates/threads/partials/utils/global_nav.html index 99213dc84..3777628cc 100644 --- a/project/threads/templates/threads/partials/utils/global_nav.html +++ b/project/threads/templates/threads/partials/utils/global_nav.html @@ -29,7 +29,7 @@ My Profile
Settings - Sign Out + Sign Out