From 93a3de57ff6bd71e551b462447dab960ed109844 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Wed, 30 Apr 2014 13:55:04 -0700 Subject: [PATCH] Created AUTHORS, MANIFEST.in, and setup.py. + Revised README.md. + AutoPEP8 rest_auth python files. --- AUTHORS | 1 + MANIFEST.in | 4 ++ README.md | 115 ++++++++++++++++++++++++++++++++++++++- rest_auth/serializers.py | 12 +++- rest_auth/tests.org.py | 75 +++++++++++++++++-------- rest_auth/tests.py | 22 +++++--- rest_auth/urls.py | 40 ++++++++------ rest_auth/utils.py | 1 + rest_auth/views.py | 15 +++-- setup.py | 46 ++++++++++++++++ 10 files changed, 276 insertions(+), 55 deletions(-) create mode 100644 AUTHORS create mode 100644 MANIFEST.in create mode 100644 setup.py diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..21906da --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +http://github.com/Tivix/django-rest-auth/contributors diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..a24b46a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include AUTHORS +include LICENSE +include MANIFEST.in +include README.md diff --git a/README.md b/README.md index 0a9d86b..9e1ecc7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,115 @@ django-rest-auth -================ +===== + +Since the introduction of django-rest-framework, Django apps have been able to serve up app-level REST API endpoints. As a result, we saw a lot of instances where developers implemented their own REST registration API endpoints here and there, snippets, and so on. We aim to solve this demand by providing django-rest-auth, a set of REST API endpoints to handle User Registration and Authentication tasks. By having these API endpoints, your client apps such as AngularJS, iOS, Android, and others can communicate to your Django backend site independently via REST APIs for User Management. Of course, we'll add more API endpoints as we see the demand. + +Features +-------- +1. User Registration with activation +2. Login/Logout +3. Retrieve/Update the Django User & user-defined UserProfile model +4. Password change +5. Password reset via e-mail + +Installation +----------- + +1. This project needs the following packages + + > django-registration>=1.0 + > + > djangorestframework>=2.3.13 + > + > django-rest-swagger>=0.1.14 + +2. Install this package. + +3. Add rest_auth app to INSTALLED\_APPS in your django settings.py + + > INSTALLED\_APPS = ( + > + > ..., + > + > 'rest_auth', + > ) + +4. This project depends on django-rest-framework library, therefore the following REST_FRAMEWORK settings needs to be entered in your Django settings.py:: + + > REST_FRAMEWORK = { + > + > 'DEFAULT_AUTHENTICATION_CLASSES': ( + > 'rest_framework.authentication.SessionAuthentication', + > ), + > + > 'DEFAULT_PERMISSION_CLASSES': ( + > 'rest_framework.permissions.IsAuthenticated' + > ) + > } + +5. Lastly, this project accepts the following Django setting values. You can set the UserProfile model and/or create your own REST registration backend for django-registration. + + > REST\_REGISTRATION\_BACKEND = 'rest\_auth.backends.rest\_registration.RESTRegistrationView' + > + > REST\_PROFILE\_MODULE = 'accounts.UserProfile' + +6. You're good to go now! + +API endpoints without Authentication +------------------------------------ + +1. /rest\_accounts/register/ - POST + + Parameters + + username, password, email, first\_name, last\_name + +2. /rest\_accounts/password/reset/ - POST + + Parameters + + email + +3. /rest\_accounts/password/reset/confirm/{uidb64}/{token}/ - POST + + Django URL Keywords + + uidb64, token + + Parameters + + new\_password1, new\_password2 + +4. /rest\_accounts/login/ - POST + + Parameters + + username, password + +5. /rest\_accounts/verify-email/{activation\_key}/ - GET + + Django URL Keywords + + activation_key + +API endpoints with Authentication +------------------------------------ + +1. /rest\_accounts/logout/ - GET + +2. /rest\_accounts/user/ - GET & POST + + GET Parameters + + POST Parameters + + user as dictionary with id, email, first\_name, last\_name + + Ex) "user": {"id": 1, "first\_name": "Person", "last\_name": "2"} + + user-defined UserProfile model fields + +3. /rest\_accounts/password/change/ - POST + + Parameters + + new\_password1, new\_password2 diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index 7526c41..9d71af0 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -12,6 +12,7 @@ class LoginSerializer(serializers.Serializer): class TokenSerializer(serializers.ModelSerializer): + """ Serializer for Token model. """ @@ -22,6 +23,7 @@ class TokenSerializer(serializers.ModelSerializer): class UserRegistrationSerializer(serializers.ModelSerializer): + """ Serializer for Django User model and most of its fields. """ @@ -32,16 +34,18 @@ class UserRegistrationSerializer(serializers.ModelSerializer): class UserRegistrationProfileSerializer(serializers.ModelSerializer): + """ Serializer that includes all profile fields except for user fk / id. """ class Meta: model = _resolve_model(getattr(settings, 'REST_PROFILE_MODULE', None)) fields = filter(lambda x: x != 'id' and x != 'user', - map(lambda x: x.name, model._meta.fields)) + map(lambda x: x.name, model._meta.fields)) class UserDetailsSerializer(serializers.ModelSerializer): + """ User model w/o password """ @@ -51,6 +55,7 @@ class UserDetailsSerializer(serializers.ModelSerializer): class UserProfileSerializer(serializers.ModelSerializer): + """ Serializer for UserProfile model. """ @@ -63,6 +68,7 @@ class UserProfileSerializer(serializers.ModelSerializer): class DynamicFieldsModelSerializer(serializers.ModelSerializer): + """ ModelSerializer that allows fields argument to control fields """ @@ -81,6 +87,7 @@ class DynamicFieldsModelSerializer(serializers.ModelSerializer): class UserUpdateSerializer(DynamicFieldsModelSerializer): + """ User model w/o username and password """ @@ -90,6 +97,7 @@ class UserUpdateSerializer(DynamicFieldsModelSerializer): class UserProfileUpdateSerializer(serializers.ModelSerializer): + """ Serializer for updating User and UserProfile model. """ @@ -102,6 +110,7 @@ class UserProfileUpdateSerializer(serializers.ModelSerializer): class SetPasswordSerializer(serializers.Serializer): + """ Serializer for changing Django User password. """ @@ -111,6 +120,7 @@ class SetPasswordSerializer(serializers.Serializer): class PasswordResetSerializer(serializers.Serializer): + """ Serializer for requesting a password reset e-mail. """ diff --git a/rest_auth/tests.org.py b/rest_auth/tests.org.py index da0f6d2..3ff1308 100644 --- a/rest_auth/tests.org.py +++ b/rest_auth/tests.org.py @@ -11,13 +11,16 @@ from registration.models import RegistrationProfile # Get the UserProfile model from the setting value -user_profile_model = _resolve_model(getattr(settings, 'REST_PROFILE_MODULE', None)) +user_profile_model = _resolve_model( + getattr(settings, 'REST_PROFILE_MODULE', None)) # Get the REST Registration Backend for django-registration -registration_backend = getattr(settings, 'REST_REGISTRATION_BACKEND', 'rest_auth.backends.rest_registration.RESTRegistrationView') +registration_backend = getattr(settings, 'REST_REGISTRATION_BACKEND', + 'rest_auth.backends.rest_registration.RESTRegistrationView') class RegistrationAndActivationTestCase(TestCase): + """ Unit Test for registering and activating a new user @@ -30,7 +33,8 @@ class RegistrationAndActivationTestCase(TestCase): def test_successful_registration(self): print 'Registering a new user' - payload = {"username": "person", "password": "person", "email": "person@world.com", "newsletter_subscribe": "false"} + payload = {"username": "person", "password": "person", + "email": "person@world.com", "newsletter_subscribe": "false"} print 'The request will attempt to register:' print 'Django User object' @@ -39,7 +43,8 @@ class RegistrationAndActivationTestCase(TestCase): print 'newsletter_subscribe: false' print 'Sending a POST request to register API' - r = requests.post(self.url, data=json.dumps(payload), headers=self.headers) + r = requests.post(self.url, data=json.dumps(payload), + headers=self.headers) if self.assertEqual(r.status_code, 201): print r.content @@ -47,10 +52,12 @@ class RegistrationAndActivationTestCase(TestCase): print 'Activating a new user' # Get the latest activation key from RegistrationProfile model - activation_key = RegistrationProfile.objects.latest('id').activation_key + activation_key = RegistrationProfile.objects.latest( + 'id').activation_key # Set the url and GET the request to verify and activate a new user - url = "http://localhost:8000/rest_auth/verify-email/" + activation_key + "/" + url = "http://localhost:8000/rest_auth/verify-email/" + \ + activation_key + "/" r = requests.get(url) print "Sending a GET request to activate the user from verify-email API" @@ -69,7 +76,8 @@ class RegistrationAndActivationTestCase(TestCase): def test_successful_registration_without_userprofile_model(self): print 'Registering a new user' - payload = {"username": "person1", "password": "person1", "email": "person1@world.com"} + payload = {"username": "person1", "password": + "person1", "email": "person1@world.com"} print 'The request will attempt to register:' print 'Django User object' @@ -77,7 +85,8 @@ class RegistrationAndActivationTestCase(TestCase): print 'No Django UserProfile object' print 'Sending a POST request to register API' - r = requests.post(self.url, data=json.dumps(payload), headers=self.headers) + r = requests.post(self.url, data=json.dumps(payload), + headers=self.headers) if self.assertEqual(r.status_code, 201): print r.content @@ -85,10 +94,12 @@ class RegistrationAndActivationTestCase(TestCase): print 'Activating a new user' # Get the latest activation key from RegistrationProfile model - activation_key = RegistrationProfile.objects.latest('id').activation_key + activation_key = RegistrationProfile.objects.latest( + 'id').activation_key # Set the url and GET the request to verify and activate a new user - url = "http://localhost:8000/rest_auth/verify-email/" + activation_key + "/" + url = "http://localhost:8000/rest_auth/verify-email/" + \ + activation_key + "/" r = requests.get(url) print "Sending a GET request to activate the user from verify-email API" @@ -112,13 +123,15 @@ class RegistrationAndActivationTestCase(TestCase): print 'The request will attempt to register with no data provided.' print 'Sending a POST request to register API' - r = requests.post(self.url, data=json.dumps(payload), headers=self.headers) + r = requests.post(self.url, data=json.dumps(payload), + headers=self.headers) if self.assertEqual(r.status_code, 400): print r.content class LoginTestCase(TestCase): + """ Unit Test for logging in @@ -137,7 +150,8 @@ class LoginTestCase(TestCase): print 'Username: %s\nPassword: %s' % ('person', 'person') print 'Sending a POST request to login API' - r = requests.post(self.url, data=json.dumps(payload), headers=self.headers) + r = requests.post(self.url, data=json.dumps(payload), + headers=self.headers) if self.assertEqual(r.status_code, 200): print r.content @@ -152,7 +166,8 @@ class LoginTestCase(TestCase): print 'Username: %s\nPassword: %s' % ('person', 'person32') print 'Sending a POST request to login API' - r = requests.post(self.url, data=json.dumps(payload), headers=self.headers) + r = requests.post(self.url, data=json.dumps(payload), + headers=self.headers) if self.assertEqual(r.status_code, 401): print r.content @@ -164,13 +179,15 @@ class LoginTestCase(TestCase): print 'The request will attempt to login with no data provided.' print 'Sending a POST request to login API' - r = requests.post(self.url, data=json.dumps(payload), headers=self.headers) + r = requests.post(self.url, data=json.dumps(payload), + headers=self.headers) if self.assertEqual(r.status_code, 400): print r.content class PasswordChangeCase(TestCase): + """ Unit Test for changing the password while logged in @@ -188,7 +205,8 @@ class PasswordChangeCase(TestCase): print 'Sending a POST request to login API' - r = requests.post(login_url, data=json.dumps(payload), headers=self.headers) + r = requests.post(login_url, data=json.dumps(payload), + headers=self.headers) if self.assertEqual(r.status_code, 200): print r.content @@ -198,18 +216,22 @@ class PasswordChangeCase(TestCase): self.token = r.json()['key'] self.headers['authorization'] = "Token " + r.json()['key'] - payload = {"new_password1": "new_person", "new_password2": "new_person"} + payload = {"new_password1": "new_person", + "new_password2": "new_person"} print 'Sending a POST request to password change API' - r = requests.post(self.url, data=json.dumps(payload), headers=self.headers) + r = requests.post(self.url, data=json.dumps(payload), + headers=self.headers) if self.assertEqual(r.status_code, 200): print r.content - payload = {"new_password1": "person", "new_password2": "person"} + payload = {"new_password1": "person", + "new_password2": "person"} print 'Sending a POST request to password change API' - r = requests.post(self.url, data=json.dumps(payload), headers=self.headers) + r = requests.post( + self.url, data=json.dumps(payload), headers=self.headers) if self.assertEqual(r.status_code, 200): print r.content @@ -221,7 +243,8 @@ class PasswordChangeCase(TestCase): print 'Sending a POST request to login API' - r = requests.post(login_url, data=json.dumps(payload), headers=self.headers) + r = requests.post(login_url, data=json.dumps(payload), + headers=self.headers) if self.assertEqual(r.status_code, 200): print r.content @@ -231,10 +254,12 @@ class PasswordChangeCase(TestCase): self.token = r.json()['key'] self.headers['authorization'] = "Token " + r.json()['key'] - payload = {"new_password1": "new_person", "new_password2": "wrong_person"} + payload = {"new_password1": "new_person", + "new_password2": "wrong_person"} print 'Sending a POST request to password change API' - r = requests.post(self.url, data=json.dumps(payload), headers=self.headers) + r = requests.post(self.url, data=json.dumps(payload), + headers=self.headers) if self.assertEqual(r.status_code, 400): print r.content @@ -246,7 +271,8 @@ class PasswordChangeCase(TestCase): print 'Sending a POST request to login API' - r = requests.post(login_url, data=json.dumps(payload), headers=self.headers) + r = requests.post(login_url, data=json.dumps(payload), + headers=self.headers) if self.assertEqual(r.status_code, 200): print r.content @@ -261,7 +287,8 @@ class PasswordChangeCase(TestCase): print 'The request will attempt to login with no data provided.' print 'Sending a POST request to password change API' - r = requests.post(self.url, data=json.dumps(payload), headers=self.headers) + r = requests.post(self.url, data=json.dumps(payload), + headers=self.headers) if self.assertEqual(r.status_code, 400): print r.content diff --git a/rest_auth/tests.py b/rest_auth/tests.py index cdd71ed..930a2bf 100644 --- a/rest_auth/tests.py +++ b/rest_auth/tests.py @@ -11,6 +11,7 @@ from django.contrib.auth.models import User class APIClient(Client): + def patch(self, path, data='', content_type=MULTIPART_CONTENT, follow=False, **extra): return self.generic('PATCH', path, data, content_type, **extra) @@ -19,6 +20,7 @@ class APIClient(Client): class CustomJSONEncoder(json.JSONEncoder): + """ Convert datetime/date objects into isoformat """ @@ -31,13 +33,15 @@ class CustomJSONEncoder(json.JSONEncoder): class BaseAPITestCase(object): + """ base for API tests: * easy request calls, f.e.: self.post(url, data), self.get(url) * easy status check, f.e.: self.post(url, data, status_code=200) """ - img = os.path.join(settings.STATICFILES_DIRS[0][1], 'images/no_profile_photo.png') + img = os.path.join( + settings.STATICFILES_DIRS[0][1], 'images/no_profile_photo.png') def send_request(self, request_method, *args, **kwargs): request_func = getattr(self.client, request_method) @@ -55,10 +59,12 @@ class BaseAPITestCase(object): kwargs['HTTP_AUTHORIZATION'] = 'Token %s' % self.token if hasattr(self, 'company_token'): - kwargs['HTTP_AUTHORIZATION'] = 'Company-Token %s' % self.company_token + kwargs[ + 'HTTP_AUTHORIZATION'] = 'Company-Token %s' % self.company_token self.response = request_func(*args, **kwargs) - is_json = bool(filter(lambda x: 'json' in x, self.response._headers['content-type'])) + is_json = bool( + filter(lambda x: 'json' in x, self.response._headers['content-type'])) if is_json and self.response.content: self.response.json = json.loads(self.response.content) else: @@ -95,7 +101,8 @@ class BaseAPITestCase(object): content_type = kwargs.pop('content_type') response = self.send_request('get', *args, **kwargs) if content_type: - self.assertEqual(bool(filter(lambda x: content_type in x, response._headers['content-type'])), True) + self.assertEqual( + bool(filter(lambda x: content_type in x, response._headers['content-type'])), True) return response def init(self): @@ -103,12 +110,11 @@ class BaseAPITestCase(object): self.client = APIClient() - # ----------------------- # T E S T H E R E # ----------------------- - class LoginAPITestCase(TestCase, BaseAPITestCase): + """ just run: python manage.py test rest_auth """ @@ -137,7 +143,6 @@ class LoginAPITestCase(TestCase, BaseAPITestCase): self.post(self.login_url, data=payload, status_code=200) self.assertEqual('key' in self.response.json.keys(), True) - self.token = self.response.json['key'] # TODO: # now all urls that required token should be available @@ -145,4 +150,5 @@ class LoginAPITestCase(TestCase, BaseAPITestCase): # TODO: - # another case to test - make user inactive and test if login is impossible + # another case to test - make user inactive and test if login is + # impossible diff --git a/rest_auth/urls.py b/rest_auth/urls.py index 9bced00..4d36448 100644 --- a/rest_auth/urls.py +++ b/rest_auth/urls.py @@ -6,24 +6,30 @@ from .views import Login, Logout, Register, UserDetails, \ urlpatterns = patterns('rest_auth.views', - # URLs that do not require a session or valid token - url(r'^register/$', Register.as_view(), name='rest_register'), - url(r'^password/reset/$', PasswordReset.as_view(), name='rest_password_reset'), - url(r'^password/reset/confirm/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', - PasswordResetConfirm.as_view(), name='rest_password_reset_confirm'), - url(r'^login/$', Login.as_view(), name='rest_login'), - url(r'^verify-email/(?P\w+)/$', - VerifyEmail.as_view(), name='verify_email'), + # URLs that do not require a session or valid token + url(r'^register/$', Register.as_view(), + name='rest_register'), + url(r'^password/reset/$', PasswordReset.as_view(), + name='rest_password_reset'), + url(r'^password/reset/confirm/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + PasswordResetConfirm.as_view( + ), name='rest_password_reset_confirm'), + url(r'^login/$', Login.as_view(), name='rest_login'), + url(r'^verify-email/(?P\w+)/$', + VerifyEmail.as_view(), name='verify_email'), - # URLs that require a user to be logged in with a valid session / token. - url(r'^logout/$', Logout.as_view(), name='rest_logout'), - url(r'^user/$', UserDetails.as_view(), name='rest_user_details'), - url(r'^password/change/$', PasswordChange.as_view(), - name='rest_password_change'), -) + # URLs that require a user to be logged in with a valid + # session / token. + url(r'^logout/$', Logout.as_view(), name='rest_logout'), + url(r'^user/$', UserDetails.as_view(), + name='rest_user_details'), + url(r'^password/change/$', PasswordChange.as_view(), + name='rest_password_change'), + ) if settings.DEBUG: urlpatterns += patterns('', - # Swagger Docs - url(r'^docs/', include('rest_framework_swagger.urls')), - ) + # Swagger Docs + url(r'^docs/', + include('rest_framework_swagger.urls')), + ) diff --git a/rest_auth/utils.py b/rest_auth/utils.py index f02f853..4b76312 100644 --- a/rest_auth/utils.py +++ b/rest_auth/utils.py @@ -3,6 +3,7 @@ from django.utils.crypto import get_random_string HASH_CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + def generate_new_hash_with_length(length): """ Generates a random string with the alphanumerical character set and given length. diff --git a/rest_auth/views.py b/rest_auth/views.py index bc6acd7..ebc5293 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -230,8 +230,9 @@ class PasswordReset(LoggedOutRESTAPIView, GenericAPIView): reset_form.save(**opts) # Return the success message with OK HTTP status - return Response({"success": "Password reset e-mail has been sent."}, - status=status.HTTP_200_OK) + return Response( + {"success": "Password reset e-mail has been sent."}, + status=status.HTTP_200_OK) else: return Response(reset_form._errors, @@ -241,7 +242,9 @@ class PasswordReset(LoggedOutRESTAPIView, GenericAPIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + class PasswordResetConfirm(LoggedOutRESTAPIView, GenericAPIView): + """ Password reset e-mail link is confirmed, therefore this resets the user's password. @@ -276,10 +279,13 @@ class PasswordResetConfirm(LoggedOutRESTAPIView, GenericAPIView): form.save() # Return the success message with OK HTTP status - return Response({"success": "Password has been reset with the new password."}, + return Response( + {"success": + "Password has been reset with the new password."}, status=status.HTTP_200_OK) else: - return Response({"error": "Invalid password reset token."}, + return Response( + {"error": "Invalid password reset token."}, status=status.HTTP_400_BAD_REQUEST) else: return Response(form._errors, status=status.HTTP_400_BAD_REQUEST) @@ -293,6 +299,7 @@ class PasswordResetConfirm(LoggedOutRESTAPIView, GenericAPIView): class VerifyEmail(LoggedOutRESTAPIView, GenericAPIView): + """ Verifies the email of the user through their activation_key. diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4a11c44 --- /dev/null +++ b/setup.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +try: + from setuptools import setup, find_packages +except ImportError: + from ez_setup import use_setuptools + use_setuptools() + from setuptools import setup, find_packages + + +import os + +here = os.path.dirname(os.path.abspath(__file__)) +f = open(os.path.join(here, 'README.md')) +long_description = f.read().strip() +f.close() + + +setup( + name='django-rest-auth', + version='0.1', + author='Sumit Chachra', + author_email='chachra@tivix.com', + url='http://github.com/Tivix/django-rest-auth', + description='Create a set of REST API endpoints for Authentication and Registration', + packages=find_packages(), + long_description=long_description, + keywords='django rest auth registration rest-framework django-registration api', + zip_safe=False, + install_requires=[ + 'Django>=1.5.0', + 'django-registration>=1.0', + 'djangorestframework>=2.3.13', + 'django-rest-swagger>=0.1.14', + ], + test_suite='rest_auth.tests', + include_package_data=True, + # cmdclass={}, + classifiers=[ + 'Framework :: Django', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Operating System :: OS Independent', + 'Topic :: Software Development' + ], +)