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
*.pyc
/libs/garden
# For Tests
# Tests
.pytest_cache
.coverage
......
......@@ -54,7 +54,8 @@ confidence=
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=unused-import,
disable=duplicate-code,
unused-import,
pointless-string-statement,
useless-super-delegation,
parameter-unpacking,
......
......@@ -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 application, profile and theme modules for settings (Shashank Kumar)
## June 9 2018
- Added SignIn feature. (Shashank Kumar)
## June 7 2018
- Sign Up feature added. (Shashank Kumar)
- Added SignUp feature. (Shashank Kumar)
## June 3 2018
......
......@@ -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_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
......
......@@ -4,7 +4,7 @@ Root Kivy Application
from kivy.app import App
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):
......@@ -30,12 +30,13 @@ if __name__ == '__main__':
'''
Setting up things
'''
initializing_database()
get_db_connection()
installing_kivy_garden_package('navigationdrawer')
# Importing modules
from modules.dashboard.dashboard import Dashboard
from modules.signup.signup import SignUp
from modules.signin.signin import SignIn
# Fixing touch issue with some platforms
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
'''
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.utils import (
generate_uuid,
......@@ -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,
calls methods to clean up full_name, convert password into
......@@ -21,26 +21,30 @@ def sign_up_user(**user_info):
Would result in a False statement if the Email is already
present.
'''
connection = initializing_database()
connection = get_db_connection()
db_cursor = connection.cursor()
user_info['table_name'] = USER_INFOMATION_TABLE
user_info['user_id'] = generate_uuid()
user_info['email'] = clean_email(user_info['email'])
user_info['password'] = hash_password(user_info['password'])
user_info['full_name'] = clean_full_name(user_info['full_name'])
user_info = {
'table_name': USER_INFOMATION_TABLE,
'user_id': generate_uuid(),
'email': clean_email(email),
'password': hash_password(password),
'full_name': clean_full_name(full_name),
'language': language,
'timezone': timezone,
}
try:
sign_up_query = '''
INSERT INTO {table_name} VALUES
("{user_id}",
"{email}",
"{password}",
"{full_name}",
"{language}",
"{timezone}")
'''
db_cursor.execute(sign_up_query.format(**user_info))
('{user_id}',
'{email}',
'{password}',
'{full_name}',
'{language}',
'{timezone}')
'''.format(**user_info)
db_cursor.execute(sign_up_query)
connection.commit()
except sqlite3.IntegrityError:
raise SignUpError('Email already exists')
......
......@@ -3,7 +3,6 @@ Class for SignUp Screen
'''
import logging
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
from kivy.clock import Clock
......@@ -26,6 +25,7 @@ class SignUp(BoxLayout, Screen):
'''
Declaration of SignUp Screen Class
'''
def prompt_error_message(self, label, error_text):
'''
Displays error message on the UI on the respective label widget
......@@ -33,6 +33,7 @@ class SignUp(BoxLayout, Screen):
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
......@@ -41,7 +42,7 @@ class SignUp(BoxLayout, Screen):
self.ids[label].text = original_text
self.ids[label].color = [1, 1, 1, 1]
logging.info(
'\'%s\' changed to \'%s\' after %s seconds',
'SignUp: \'%s\' changed to \'%s\' after %s seconds',
error_text,
original_text,
args[0]
......@@ -53,50 +54,50 @@ class SignUp(BoxLayout, Screen):
Validating Email, Password and Full Name provided by user
'''
email_validation = True
pass_validation = True
full_name_validation = True
name_validation = True
password_validation = True
user_email = self.ids['user_email'].text
try:
validate_email(user_email)
except SignUpError as error:
email_validation = False
self.prompt_error_message(
'email_label',
error.message,
)
email_validation = False
first_pass = self.ids['first_pass'].text
try:
validate_first_pass(first_pass)
except SignUpError as error:
pass_validation = False
self.prompt_error_message(
'first_pass_label',
error.message,
)
password_validation = False
confirm_pass = self.ids['confirm_pass'].text
try:
validate_confirm_pass(first_pass, confirm_pass)
except SignUpError as error:
pass_validation = False
self.prompt_error_message(
'confirm_pass_label',
error.message,
)
password_validation = False
full_name = self.ids['user_full_name'].text
try:
validate_full_name(full_name)
except SignUpError as error:
full_name_validation = False
self.prompt_error_message(
'full_name_label',
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):
'''
......@@ -105,20 +106,19 @@ class SignUp(BoxLayout, Screen):
'''
app_object = args[0]
if self.validate():
user_email = self.ids['user_email'].text
user_pass = self.ids['first_pass'].text
user_full_name = self.ids['user_full_name'].text
user_language = self.ids['user_language'].text
user_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
}
email = self.ids['user_email'].text
password = self.ids['first_pass'].text
full_name = self.ids['user_full_name'].text
language = self.ids['user_language'].text
timezone = self.ids['user_timezone'].text
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()
except SignUpError as error:
self.prompt_error_message(
......
......@@ -31,5 +31,4 @@ def hash_password(password):
'''
hash_password converts plain text password into sha256 hash
'''
hased_pass = hashlib.sha256(password.encode()).hexdigest()
return hased_pass
return hashlib.sha256(password.encode()).hexdigest()
......@@ -14,9 +14,9 @@ DATABASE_FILE = 'new_contributor_wizard.db'
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)
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
from settings import (
DATABASE_FILE,
USER_INFOMATION_TABLE,
initializing_database
get_db_connection
)
def setup():
#setting up database schema
initializing_database()
get_db_connection()
def testing_setting_constants():
......@@ -45,7 +45,7 @@ def teardown():
connection = sqlite3.connect(DATABASE_FILE)
db_cursor = connection.cursor()
db_cursor.execute('''
DELETE FROM USERS WHERE USERS.email="abc@shanky.xyz"
DELETE FROM USERS WHERE USERS.email='abc@shanky.xyz'
''')
connection.commit()
connection.close()
<Dashboard>:
BoxLayout:
size_hint: 0.3, 1
orientation: 'vertical'
BoxLayout:
size_hint: 1, 0.1
BoxLayout:
size_hint: 1, 0.8
orientation: 'vertical'
id: dashboard_menu
BoxLayout:
size_hint: 1, 0.142
padding: 0, 0, 10, 0
id: how_to_use_box
canvas.before:
Color:
rgba: 0, 0, 0, 1
Rectangle:
pos: self.pos
size: self.size
Label:
text_size: self.size
size: self.texture_size
valign: 'middle'
halign: 'right'
font_name: 'ui/assets/fonts/VarelaRound-Regular.ttf'
font_size: 20
text: 'How To Use'
id: how_to_use_label
BoxLayout:
size_hint: 1, 0.142
padding: 0, 0, 10, 0
id: communication_box
canvas.before:
Color:
rgba: 0, 0, 0, 1
Rectangle:
pos: self.pos
size: self.size
Label:
text: 'Communication'
text_size: self.size
size: self.texture_size
valign: 'middle'
halign: 'right'
font_name: 'ui/assets/fonts/VarelaRound-Regular.ttf'
font_size: 20
id: communication_label
BoxLayout:
size_hint: 1, 0.142
padding: 0, 0, 10, 0
id: cli_box
canvas.before:
Color:
rgba: 0, 0, 0, 1
Rectangle:
pos: self.pos
size: self.size
Label:
text: 'CLI'
text_size: self.size
size: self.texture_size
valign: 'middle'
halign: 'right'
font_name: 'ui/assets/fonts/VarelaRound-Regular.ttf'
font_size: 20
id: cli_label
BoxLayout:
size_hint: 1, 0.142
padding: 0, 0, 10, 0
id: blog_box
canvas.before:
Color:
rgba: 0, 0, 0, 1
Rectangle:
pos: self.pos
size: self.size
Label:
text: 'Blog'
text_size: self.size
size: self.texture_size
valign: 'middle'
halign: 'right'
font_name: 'ui/assets/fonts/VarelaRound-Regular.ttf'
font_size: 20
id: blog_label
BoxLayout:
size_hint: 1, 0.142
padding: 0, 0, 10, 0
id: vcs_box
canvas.before:
Color:
rgba: 0, 0, 0, 1
Rectangle:
pos: self.pos
size: self.size
Label:
text: 'Version Control'
text_size: self.size
size: self.texture_size
valign: 'middle'
halign: 'right'
font_name: 'ui/assets/fonts/VarelaRound-Regular.ttf'
font_size: 20
id: vcs_label
BoxLayout:
size_hint: 1, 0.142
padding: 0, 0, 10, 0
id: encryption_box
canvas.before:
Color:
rgba: 0, 0, 0, 1
Rectangle:
pos: self.pos
size: self.size
Label: