Commit 05b5d18a authored by Sanyam Khurana's avatar Sanyam Khurana

feat(sign-in): Add sign-in feature for users.

Adds SignIn feature

See merge request new-contributor-wizard-team/new-contributor-wizard!5
parents db6ba72b 5227e2c1
# Project Tools
modules/encryption/tools/all_tools/gnupg_home
# Python # Python
*.pyc *.pyc
/libs/garden /libs/garden
# For Tests # Tests
.pytest_cache .pytest_cache
.coverage .coverage
......
...@@ -54,7 +54,8 @@ confidence= ...@@ -54,7 +54,8 @@ confidence=
# --enable=similarities". If you want to run only the classes checker, but have # --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes # no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W" # --disable=W"
disable=unused-import, disable=duplicate-code,
unused-import,
pointless-string-statement, pointless-string-statement,
useless-super-delegation, useless-super-delegation,
parameter-unpacking, parameter-unpacking,
......
...@@ -6,9 +6,13 @@ v0.0.1 ...@@ -6,9 +6,13 @@ v0.0.1
- Adding blog, cli, communication, encryption, how_to_use, vcs and way_ahead modules for courseware (Shashank Kumar) - Adding blog, cli, communication, encryption, how_to_use, vcs and way_ahead modules for courseware (Shashank Kumar)
- Adding application, profile and theme modules for settings (Shashank Kumar) - Adding application, profile and theme modules for settings (Shashank Kumar)
## June 9 2018
- Added SignIn feature. (Shashank Kumar)
## June 7 2018 ## June 7 2018
- Sign Up feature added. (Shashank Kumar) - Added SignUp feature. (Shashank Kumar)
## June 3 2018 ## June 3 2018
......
...@@ -110,7 +110,24 @@ There are two type of testing being done for this application. ...@@ -110,7 +110,24 @@ There are two type of testing being done for this application.
- `test_services.py` - contains tests for `modules/signup/services.py` - `test_services.py` - contains tests for `modules/signup/services.py`
- `test_utils.py` - contains tests for `modules/signup/utils.py` - `test_utils.py` - contains tests for `modules/signup/utils.py`
- `test_validation.py` - contains tests for `modules/signup/validations.py`
### SignIn Module
`modules/signin/` contains the Python logic for Sign In. They can be described as below.
- `exceptions.py` contains SignIn module specific custom exception classes
- `services.py` contains service functions for SignIn module
- `signin.py` contains KIVY UI and other integrations for SignIn module
- `validations.py` contains user information validation functions
- `utils.py` contains utility functions to help out with SignIn operations
- `ui/signin.kv` file contains KIVY widget tree which in turn renders the UI for the Sign Up module.
- `tests/signin/` contains written tests for the Sign Up module. They can be described as below.
- `test_services.py` - contains tests for `modules/signin/services.py`
- `test_utils.py` - contains tests for `modules/signin/utils.py`
- `test_validation.py` - contains tests for `modules/signin/validations.py`
### The Dashboard ### The Dashboard
......
...@@ -4,7 +4,7 @@ Root Kivy Application ...@@ -4,7 +4,7 @@ Root Kivy Application
from kivy.app import App from kivy.app import App
from kivy.config import Config from kivy.config import Config
from settings import initializing_database, installing_kivy_garden_package from settings import get_db_connection, installing_kivy_garden_package
class NewContributorWizard(App): class NewContributorWizard(App):
...@@ -30,12 +30,13 @@ if __name__ == '__main__': ...@@ -30,12 +30,13 @@ if __name__ == '__main__':
''' '''
Setting up things Setting up things
''' '''
initializing_database() get_db_connection()
installing_kivy_garden_package('navigationdrawer') installing_kivy_garden_package('navigationdrawer')
# Importing modules # Importing modules
from modules.dashboard.dashboard import Dashboard from modules.dashboard.dashboard import Dashboard
from modules.signup.signup import SignUp from modules.signup.signup import SignUp
from modules.signin.signin import SignIn
# Fixing touch issue with some platforms # Fixing touch issue with some platforms
Config.set('input', 'mouse', 'mouse') Config.set('input', 'mouse', 'mouse')
......
'''
This class contains signin module specific exceptions
'''
class SignInError(Exception):
'''
SignInError class can be used to raise exception related to
Sign In module
'''
def __init__(self, message):
self.message = message
super().__init__(message)
'''
This modules contain classes to query sqlite3 database
'''
from settings import get_db_connection, USER_INFOMATION_TABLE
from modules.signin.exceptions import SignInError
from modules.signin.utils import (
clean_email,
hash_password
)
def sign_in_user(email, password):
'''
sign_in_user would try to check for user's email and hashed
password in the database
Would result in a UserError if email doesn't exist
Would result in a PasswordError if password doesn't match
'''
connection = get_db_connection()
db_cursor = connection.cursor()
cleaned_email = clean_email(email)
hashed_pass = hash_password(password)
sign_in_query = 'SELECT * FROM {} WHERE email=?'.format(USER_INFOMATION_TABLE)
user_info = db_cursor.execute(sign_in_query, (cleaned_email, )).fetchone()
if not user_info:
raise SignInError('Email does not exist!')
elif user_info[2] != hashed_pass:
raise SignInError('Password is incorrect!')
return user_info
'''
Class for SignIn Screen
'''
import logging
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
from kivy.clock import Clock
from kivy.uix.screenmanager import Screen
from modules.signin.services import sign_in_user
from modules.signin.exceptions import SignInError
from modules.signin.validations import validate_email, validate_password
Builder.load_file('./ui/signin.kv')
class SignIn(BoxLayout, Screen):
'''
Declaration of SignIn Screen Class
'''
def prompt_error_message(self, label, error_text):
'''
Displays error message on the UI on the respective label widget
'''
original_text = self.ids[label].text
self.ids[label].text = error_text
self.ids[label].color = [1, 0, 0, 1]
def replace_label(*args):
'''
Replacing original text in label
delay time is defined by args[0]
'''
logging.info(
'SignIn: \'%s\' changed to \'%s\' after %s seconds',
error_text,
original_text,
args[0]
)
self.ids[label].text = original_text
self.ids[label].color = [1, 1, 1, 1]
Clock.schedule_once(replace_label, 2)
def validate(self):
'''
Validating Email and Password provided by user
'''
email_validation = True
password_validation = True
user_email = self.ids['user_email'].text
try:
validate_email(user_email)
except SignInError as error:
self.prompt_error_message(
'email_label',
error.message,
)
email_validation = False
password = self.ids['password'].text
try:
validate_password(password)
except SignInError as error:
self.prompt_error_message(
'password_label',
error.message,
)
password_validation = False
return email_validation and password_validation
def sign_in(self):
'''
Signin user in case of successful validation
Prompting error message to the user otherwise
'''
if self.validate():
user_email = self.ids['user_email'].text
user_pass = self.ids['password'].text
try:
sign_in_user(
email=user_email,
password=user_pass
)
self.manager.transition.direction = 'left'
self.manager.current = 'dashboard'
except SignInError as error:
self.prompt_error_message(
'email_label',
error.message
)
'''
This module contains utility functions
'''
import hashlib
def clean_email(user_email):
'''
clean_email removes unnecessary spaces from Full Name
'''
return user_email.strip('\t\n\r ')
def hash_password(password):
'''
hash_password converts plain text password into sha256 hash
'''
return hashlib.sha256(password.encode()).hexdigest()
'''
This module contains Validation functions
'''
import re
from modules.signin.utils import clean_email
from modules.signin.exceptions import SignInError
def validate_email(user_email):
'''
Validating Email provided to check for proper mail format and
only alphabets
'''
user_email = clean_email(user_email)
if not re.match(r'[^@]+@[^@]+\.[^@]+', user_email):
raise SignInError('Incorrect Format')
user_email = user_email.strip(' ').split('.')
for parts in user_email:
parts = parts.split('@')
if not all(part.isalpha() for part in parts):
raise SignInError('Incorrect Format')
return True
def validate_password(password):
'''
Validating whether or not the password is submitted by
the user
'''
if not password:
raise SignInError('Enter password')
return True
...@@ -3,7 +3,7 @@ This module contains services to be utilized by the application ...@@ -3,7 +3,7 @@ This module contains services to be utilized by the application
''' '''
import sqlite3 import sqlite3
from settings import initializing_database, USER_INFOMATION_TABLE from settings import get_db_connection, USER_INFOMATION_TABLE
from modules.signup.exceptions import SignUpError from modules.signup.exceptions import SignUpError
from modules.signup.utils import ( from modules.signup.utils import (
generate_uuid, generate_uuid,
...@@ -13,7 +13,7 @@ from modules.signup.utils import ( ...@@ -13,7 +13,7 @@ from modules.signup.utils import (
) )
def sign_up_user(**user_info): def sign_up_user(email, password, full_name, language, timezone):
''' '''
sign_up_user creates connection with the sqlite3 database, sign_up_user creates connection with the sqlite3 database,
calls methods to clean up full_name, convert password into calls methods to clean up full_name, convert password into
...@@ -21,26 +21,30 @@ def sign_up_user(**user_info): ...@@ -21,26 +21,30 @@ def sign_up_user(**user_info):
Would result in a False statement if the Email is already Would result in a False statement if the Email is already
present. present.
''' '''
connection = initializing_database() connection = get_db_connection()
db_cursor = connection.cursor() db_cursor = connection.cursor()
user_info['table_name'] = USER_INFOMATION_TABLE user_info = {
user_info['user_id'] = generate_uuid() 'table_name': USER_INFOMATION_TABLE,
user_info['email'] = clean_email(user_info['email']) 'user_id': generate_uuid(),
user_info['password'] = hash_password(user_info['password']) 'email': clean_email(email),
user_info['full_name'] = clean_full_name(user_info['full_name']) 'password': hash_password(password),
'full_name': clean_full_name(full_name),
'language': language,
'timezone': timezone,
}
try: try:
sign_up_query = ''' sign_up_query = '''
INSERT INTO {table_name} VALUES INSERT INTO {table_name} VALUES
("{user_id}", ('{user_id}',
"{email}", '{email}',
"{password}", '{password}',
"{full_name}", '{full_name}',
"{language}", '{language}',
"{timezone}") '{timezone}')
''' '''.format(**user_info)
db_cursor.execute(sign_up_query.format(**user_info)) db_cursor.execute(sign_up_query)
connection.commit() connection.commit()
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
raise SignUpError('Email already exists') raise SignUpError('Email already exists')
......
...@@ -3,7 +3,6 @@ Class for SignUp Screen ...@@ -3,7 +3,6 @@ Class for SignUp Screen
''' '''
import logging import logging
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder from kivy.lang import Builder
from kivy.clock import Clock from kivy.clock import Clock
...@@ -26,6 +25,7 @@ class SignUp(BoxLayout, Screen): ...@@ -26,6 +25,7 @@ class SignUp(BoxLayout, Screen):
''' '''
Declaration of SignUp Screen Class Declaration of SignUp Screen Class
''' '''
def prompt_error_message(self, label, error_text): def prompt_error_message(self, label, error_text):
''' '''
Displays error message on the UI on the respective label widget Displays error message on the UI on the respective label widget
...@@ -33,6 +33,7 @@ class SignUp(BoxLayout, Screen): ...@@ -33,6 +33,7 @@ class SignUp(BoxLayout, Screen):
original_text = self.ids[label].text original_text = self.ids[label].text
self.ids[label].text = error_text self.ids[label].text = error_text
self.ids[label].color = [1, 0, 0, 1] self.ids[label].color = [1, 0, 0, 1]
def replace_label(*args): def replace_label(*args):
''' '''
Replacing original text in label Replacing original text in label
...@@ -41,7 +42,7 @@ class SignUp(BoxLayout, Screen): ...@@ -41,7 +42,7 @@ class SignUp(BoxLayout, Screen):
self.ids[label].text = original_text self.ids[label].text = original_text
self.ids[label].color = [1, 1, 1, 1] self.ids[label].color = [1, 1, 1, 1]
logging.info( logging.info(
'\'%s\' changed to \'%s\' after %s seconds', 'SignUp: \'%s\' changed to \'%s\' after %s seconds',
error_text, error_text,
original_text, original_text,
args[0] args[0]
...@@ -53,50 +54,50 @@ class SignUp(BoxLayout, Screen): ...@@ -53,50 +54,50 @@ class SignUp(BoxLayout, Screen):
Validating Email, Password and Full Name provided by user Validating Email, Password and Full Name provided by user
''' '''
email_validation = True email_validation = True
pass_validation = True name_validation = True
full_name_validation = True password_validation = True
user_email = self.ids['user_email'].text user_email = self.ids['user_email'].text
try: try:
validate_email(user_email) validate_email(user_email)
except SignUpError as error: except SignUpError as error:
email_validation = False
self.prompt_error_message( self.prompt_error_message(
'email_label', 'email_label',
error.message, error.message,
) )
email_validation = False
first_pass = self.ids['first_pass'].text first_pass = self.ids['first_pass'].text
try: try:
validate_first_pass(first_pass) validate_first_pass(first_pass)
except SignUpError as error: except SignUpError as error:
pass_validation = False
self.prompt_error_message( self.prompt_error_message(
'first_pass_label', 'first_pass_label',
error.message, error.message,
) )
password_validation = False
confirm_pass = self.ids['confirm_pass'].text confirm_pass = self.ids['confirm_pass'].text
try: try:
validate_confirm_pass(first_pass, confirm_pass) validate_confirm_pass(first_pass, confirm_pass)
except SignUpError as error: except SignUpError as error:
pass_validation = False
self.prompt_error_message( self.prompt_error_message(
'confirm_pass_label', 'confirm_pass_label',
error.message, error.message,
) )
password_validation = False
full_name = self.ids['user_full_name'].text full_name = self.ids['user_full_name'].text
try: try:
validate_full_name(full_name) validate_full_name(full_name)
except SignUpError as error: except SignUpError as error:
full_name_validation = False
self.prompt_error_message( self.prompt_error_message(
'full_name_label', 'full_name_label',
error.message, error.message,
) )
name_validation = False
return email_validation and pass_validation and full_name_validation return email_validation and name_validation and password_validation
def sign_up(self, *args): def sign_up(self, *args):
''' '''
...@@ -105,20 +106,19 @@ class SignUp(BoxLayout, Screen): ...@@ -105,20 +106,19 @@ class SignUp(BoxLayout, Screen):
''' '''
app_object = args[0] app_object = args[0]
if self.validate(): if self.validate():
user_email = self.ids['user_email'].text email = self.ids['user_email'].text
user_pass = self.ids['first_pass'].text password = self.ids['first_pass'].text
user_full_name = self.ids['user_full_name'].text full_name = self.ids['user_full_name'].text
user_language = self.ids['user_language'].text language = self.ids['user_language'].text
user_timezone = self.ids['user_timezone'].text timezone = self.ids['user_timezone'].text
user_info = {
'email': user_email,
'password': user_pass,
'full_name': user_full_name,
'language': user_language,
'timezone': user_timezone
}
try: try:
sign_up_user(**user_info) sign_up_user(
email=email,
password=password,
full_name=full_name,
language=language,
timezone=timezone
)
app_object.switch_screen_to_dashboard() app_object.switch_screen_to_dashboard()
except SignUpError as error: except SignUpError as error:
self.prompt_error_message( self.prompt_error_message(
......
...@@ -31,5 +31,4 @@ def hash_password(password): ...@@ -31,5 +31,4 @@ def hash_password(password):
''' '''
hash_password converts plain text password into sha256 hash hash_password converts plain text password into sha256 hash
''' '''
hased_pass = hashlib.sha256(password.encode()).hexdigest() return hashlib.sha256(password.encode()).hexdigest()
return hased_pass
...@@ -14,9 +14,9 @@ DATABASE_FILE = 'new_contributor_wizard.db' ...@@ -14,9 +14,9 @@ DATABASE_FILE = 'new_contributor_wizard.db'
USER_INFOMATION_TABLE = 'USERS' USER_INFOMATION_TABLE = 'USERS'
def initializing_database(): def get_db_connection():
''' '''
Creating database file is not present Creating connection to the database and schema if required
''' '''
connection = sqlite3.connect(DATABASE_FILE) connection = sqlite3.connect(DATABASE_FILE)
db_cursor = connection.cursor() db_cursor = connection.cursor()
......
import pytest
from modules.signin.services import sign_in_user
from modules.signin.exceptions import SignInError
from settings import (
get_db_connection
)
from modules.signin.utils import (
clean_email,
hash_password
)
def setup():
# inserting test values to the database
connection = get_db_connection()
db_cursor = connection.cursor()
cleaned_email = clean_email('shanky@shanky.xyz')
hashed_password = hash_password('mynewpass')
db_cursor.execute('''
INSERT INTO USERS (email, password) VALUES ('{}', '{}')
'''.format(cleaned_email, hashed_password)
)
connection.commit()
def test_sign_in():
# checking valid login
email = 'shanky@shanky.xyz'
password = 'mynewpass'
user_info = sign_in_user(
email=email,
password=password
)
assert user_info
# checking invalid login for no account
email = 'shashankkumarkushwaha@gmail.com'
password = 'mynewpass'
with pytest.raises(SignInError):
sign_in_user(
email=email,
password=password
)
# checking invalid login for incorrect password
email = 'shanky@shanky.xyz'
password = 'myoldpass'
with pytest.raises(SignInError):
sign_in_user(
email=email,
password=password
)
def teardown():
# deleting test values from the database
connection = get_db_connection()
db_cursor = connection.cursor()
db_cursor.execute('''
DELETE FROM USERS WHERE USERS.email='shanky@shanky.xyz'
''')
connection.commit()
connection.close()
from modules.signin.utils import clean_email
def test_clean_email():
# checking strip operation on email
assert clean_email('abc@shanky.xyz ') == 'abc@shanky.xyz'
assert clean_email(' abc@shanky.xyz ') == 'abc@shanky.xyz'
assert clean_email('abc@shanky.xyz ') == 'abc@shanky.xyz'
assert clean_email('\nabc@shanky.xyz ') == 'abc@shanky.xyz'
import pytest
from modules.signin.exceptions import SignInError
from modules.signin.validations import (
validate_email,
validate_password
)
def test_validate_email():
# validating stip operation and correct email format
assert validate_email('abc@shanky.xyz')
assert validate_email(' abc@shanky.xyz')
assert validate_email('abc@shanky.xyz ')
assert validate_email(' abc@shanky.xyz ')
# validating incorrect email format
with pytest.raises(SignInError):
assert not validate_email('abcshanky.xyz')
with pytest.raises(SignInError):
assert not validate_email('abc@!shanky.xyz')
# validating empty email input
with pytest.raises(SignInError):
assert not validate_email('')
with pytest.raises(SignInError):
assert not validate_email(' ')
def test_validate_password():
# validating correct email format
assert validate_password('mynewpass')
# validating empty password input
with pytest.raises(SignInError):
assert not validate_password('')
...@@ -7,13 +7,13 @@ from modules.signup.exceptions import SignUpError ...@@ -7,13 +7,13 @@ from modules.signup.exceptions import SignUpError
from settings import ( from settings import (
DATABASE_FILE, DATABASE_FILE,
USER_INFOMATION_TABLE, USER_INFOMATION_TABLE,
initializing_database get_db_connection
) )
def setup(): def setup():
#setting up database schema #setting up database schema
initializing_database() get_db_connection()
def testing_setting_constants(): def testing_setting_constants():
...@@ -45,7 +45,7 @@ def teardown(): ...@@ -45,7 +45,7 @@ def teardown():
connection = sqlite3.connect(DATABASE_FILE) connection = sqlite3.connect(DATABASE_FILE)