Commit d6b46042 authored by Sanyam Khurana's avatar Sanyam Khurana

Merge branch 'adding-signup-feature' into 'master'

Adds SignUp feature

See merge request new-contributor-wizard-team/new-contributor-wizard!4
parents 8f3ca735 7d852bdb
# For Tests
.pytest_cache .pytest_cache
.coverage .coverage
# Database File
new_contributor_wizard.db
...@@ -12,11 +12,12 @@ before_script: ...@@ -12,11 +12,12 @@ before_script:
pylint: pylint:
type: test type: test
script: script:
- pipenv run pylint main.py - pipenv run pylint main
- pipenv run pylint settings
- pipenv run pylint modules - pipenv run pylint modules
pytest: pytest:
type: test type: test
script: script:
- pipenv run pytest tests/ - pipenv run pytest tests
- pipenv run pytest --cov=modules - pipenv run pytest --cov=modules
v0.0.1 v0.0.1
## June 7 2018
- Sign Up feature added.
## June 11 2018 ## June 11 2018
......
...@@ -78,6 +78,8 @@ There are two type of testing being done for this application. ...@@ -78,6 +78,8 @@ There are two type of testing being done for this application.
- `main.py` - It contains the Root Kivy Application which is to run in order to start the GUI. - `main.py` - It contains the Root Kivy Application which is to run in order to start the GUI.
- `settings.py` - This modules sets up the database which is later integrated with the entire application.
- `data` - It contains static application data which can be used by application at anytime. It helps when the machine is offine. - `data` - It contains static application data which can be used by application at anytime. It helps when the machine is offine.
- `docs` - This directory should and only contain the documentations for developers, contributors and end users. - `docs` - This directory should and only contain the documentations for developers, contributors and end users.
...@@ -86,7 +88,25 @@ There are two type of testing being done for this application. ...@@ -86,7 +88,25 @@ There are two type of testing being done for this application.
- `tests` - This directory should and only contain the Test written for the application, both for application logic and GUI. - `tests` - This directory should and only contain the Test written for the application, both for application logic and GUI.
- `ui` - This directory should and only contain the `.kv` files which uses Kivy Language in order to create the widget tree. Hence, making the UI designing easier. - `ui` - This directory should and only contain the `.kv` files which uses Kivy Language in order to create the widget tree.
### Sign Up Module
- `modules/signup/` contains the Python logic for Sign Up. They can be described as below.
- `exceptions.py` contains SignUp module specific custom exception classes
- `services.py` contains service functions for SignUp module
- `signup.py` contains KIVY UI and other integrations for SignUp module
- `validations.py` contains user information validation functions
- `utils.py` contains utility functions to help out with SignUp operations
- `ui/signup.kv` file contains KIVY widget tree which in turn renders the UI for the Sign Up module.
- `tests/signup/` contains written tests for the Sign Up module. They can be described as below.
- `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`
### The Dashboard ### The Dashboard
...@@ -119,4 +139,3 @@ There are two type of testing being done for this application. ...@@ -119,4 +139,3 @@ There are two type of testing being done for this application.
- `application.py` module contains application specific settings like language - `application.py` module contains application specific settings like language
- `profile.py` module contains user related settings like name, email etc - `profile.py` module contains user related settings like name, email etc
- `theme.py` module contains application theme related settings like color - `theme.py` module contains application theme related settings like color
...@@ -3,25 +3,39 @@ Root Kivy Application ...@@ -3,25 +3,39 @@ Root Kivy Application
''' '''
from kivy.app import App from kivy.app import App
from kivy.config import Config from kivy.config import Config
from kivy.uix.screenmanager import ScreenManager
from modules.dashboard.dashboard import Dashboard from settings import initializing_database
from modules.signup import signup
from modules.dashboard import dashboard
class NewContributorWizard(App): class NewContributorWizard(App):
''' '''
Declaration of Root Kivy App which contains Root Widget Declaration of Root Kivy App which contains Root Widget
''' '''
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen_manager_obj = ScreenManager()
self.screen_manager_obj.add_widget(signup.SignUp(name='signup'))
self.screen_manager_obj.add_widget(dashboard.Dashboard(name='dashboard'))
def build(self): def build(self):
return Dashboard() return self.screen_manager_obj
if __name__ == '__main__': if __name__ == '__main__':
'''
Setting up things
'''
initializing_database()
''' '''
Setting window width to 720px and height to 480px Setting window width to 720px and height to 480px
''' '''
Config.set('graphics', 'width', '720') Config.set('graphics', 'width', '720')
Config.set('graphics', 'height', '480') Config.set('graphics', 'height', '480')
Config.set('graphics', 'resizable', False)
''' '''
Running Kivy application and building root Widget Running Kivy application and building root Widget
......
...@@ -5,6 +5,7 @@ from copy import copy as cp ...@@ -5,6 +5,7 @@ from copy import copy as cp
from kivy.uix.boxlayout import BoxLayout from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder from kivy.lang import Builder
from kivy.uix.screenmanager import Screen
from modules.blog.blog import Blog from modules.blog.blog import Blog
from modules.cli.cli import CLI from modules.cli.cli import CLI
...@@ -21,12 +22,12 @@ from modules.settings.theme import ThemeSettings ...@@ -21,12 +22,12 @@ from modules.settings.theme import ThemeSettings
Builder.load_file('./ui/dashboard.kv') Builder.load_file('./ui/dashboard.kv')
class Dashboard(BoxLayout): class Dashboard(BoxLayout, Screen):
''' '''
Dashboard class to integrate courseware and settings Dashboard class to integrate courseware and settings
''' '''
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(Dashboard, self).__init__(**kwargs) super().__init__(**kwargs)
self.all_menu_screens = { self.all_menu_screens = {
'application_settings': ApplicationSettings(), 'application_settings': ApplicationSettings(),
'blog': Blog(), 'blog': Blog(),
...@@ -40,7 +41,6 @@ class Dashboard(BoxLayout): ...@@ -40,7 +41,6 @@ class Dashboard(BoxLayout):
'way_ahead': WayAhead(), 'way_ahead': WayAhead(),
} }
self.all_menu_items = list(self.all_menu_screens.keys()) self.all_menu_items = list(self.all_menu_screens.keys())
self.enable_menu('how_to_use') self.enable_menu('how_to_use')
def on_touch_down(self, touch): def on_touch_down(self, touch):
......
'''
Exceptions module contains exception classes specific to signup module
'''
class SignUpError(Exception):
'''
SignUpError class can be used to raise exception related to
User's imput
'''
def __init__(self, message):
self.message = message
super().__init__(message)
'''
This module contains services to be utilized by the application
'''
import sqlite3
from settings import initializing_database, USER_INFOMATION_TABLE
from modules.signup.exceptions import SignUpError
from modules.signup.utils import (
generate_uuid,
clean_email,
clean_full_name,
hash_password
)
def sign_up_user(**user_info):
'''
sign_up_user creates connection with the sqlite3 database,
calls methods to clean up full_name, convert password into
hash and query database to save user's information.
Would result in a False statement if the Email is already
present.
'''
connection = initializing_database()
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'])
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))
connection.commit()
except sqlite3.IntegrityError:
raise SignUpError('Email already exists')
return True
'''
Class for SignUp 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.signup.services import sign_up_user
from modules.signup.exceptions import SignUpError
from modules.signup.validations import (
validate_email,
validate_first_pass,
validate_confirm_pass,
validate_full_name
)
Builder.load_file('./ui/signup.kv')
class SignUp(BoxLayout, Screen):
'''
Declaration of SignUp Screen Class
'''
def __init__(self, **kwargs):
'''
Creating instance of vaidation class
Creating instance of updateBD class
'''
super().__init__(**kwargs)
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 = (255, 0, 0, 1)
def replace_label(*args):
'''
Replacing original text in label
delay time is defined by args[0]
'''
logging.info(
'\'%s\' changed to \'%s\' after %s seconds',
error_text,
original_text,
args[0]
)
self.ids[label].text = original_text
self.ids[label].color = (255, 255, 255, 1)
Clock.schedule_once(replace_label, 2)
def validate(self):
'''
Validating Email, Password and Full Name provided by user
'''
email_validation = True
pass_validation = True
full_name_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,
)
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,
)
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,
)
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,
)
return email_validation and pass_validation and full_name_validation
def sign_up(self):
'''
Signing Up user's account 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['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
}
try:
sign_up_user(**user_info)
self.manager.current = 'dashboard'
except SignUpError as error:
self.prompt_error_message(
'email_label',
error.message
)
'''
This module contains utility functions
'''
import hashlib
from uuid import uuid4
def generate_uuid():
'''
generate_uuid return a universally unique identifier string
'''
return str(uuid4())
def clean_email(user_email):
'''
clean_email removes unnecessary spaces from Full Name
'''
return user_email.strip('\t\n\r ')
def clean_full_name(full_name):
'''
clean_full_name removes unnecessary spaces from Full Name
'''
full_name = ' '.join(full_name.split())
return full_name.strip('\t\n\r ')
def hash_password(password):
'''
hash_password converts plain text password into sha256 hash
'''
hased_pass = hashlib.sha256(password.encode()).hexdigest()
return hased_pass
'''
This module contains Validation functions
'''
import re
from modules.signup.exceptions import SignUpError
from modules.signup.utils import (
clean_email,
clean_full_name)
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 SignUpError('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 SignUpError('Incorrect Format')
return True
def validate_first_pass(first_pass):
'''
Validating whether or not the first password is submitted by
the user
'''
if not first_pass:
raise SignUpError('Enter password')
elif len(first_pass) < 6:
raise SignUpError('Password is short')
return True
def validate_confirm_pass(first_pass, confirm_pass):
'''
Validating whether both the password submitted by the user
match each other
'''
if first_pass != confirm_pass:
raise SignUpError('Password is not same')
return True
def validate_full_name(full_name):
'''
Validating whether Full Name is provided by user and is in
correct alphabet format
'''
full_name = clean_full_name(full_name)
if not full_name:
raise SignUpError('Enter Full Name')
for name in full_name.split():
if not name.isalpha():
raise SignUpError('Incorrect Format')
return True
'''
Initializing application dependencies
- Sqlite3 database
'''
import sqlite3
DATABASE_FILE = 'new_contributor_wizard.db'
USER_INFOMATION_TABLE = 'USERS'
def initializing_database():
'''
Creating database file is not present
'''
connection = sqlite3.connect(DATABASE_FILE)
db_cursor = connection.cursor()
try:
db_cursor.execute('''
CREATE TABLE USERS
(id VARCHAR(36) PRIMARY KEY,
email UNIQUE,
password VARCHAR(64),
fullname TEXT,
language TEXT,
timezone TEXT)
''')
connection.commit()
return connection
except sqlite3.OperationalError:
return connection
import pytest
import sqlite3
import os
from modules.signup.services import sign_up_user
from modules.signup.exceptions import SignUpError
from settings import (
DATABASE_FILE,
USER_INFOMATION_TABLE,
initializing_database
)
def setup():
#setting up database schema
initializing_database()
def testing_setting_constants():
# testing application constants
assert DATABASE_FILE
assert USER_INFOMATION_TABLE
def test_signup_operation():
# test values
user_info = {
'email': 'abc@shanky.xyz',
'password': 'mynewpass',
'full_name': 'Shashank Kumar',
'language':'English',
'timezone': 'UTC+5:30',
}
# testing valid signup
assert sign_up_user(**user_info)
# testing invalid signup
with pytest.raises(SignUpError):
sign_up_user(**user_info)
def teardown():
# deleting test values
connection = sqlite3.connect(DATABASE_FILE)
db_cursor = connection.cursor()
db_cursor.execute('''
DELETE FROM USERS WHERE USERS.email="abc@shanky.xyz"
''')
connection.commit()
connection.close()
import pytest
from modules.signup.utils import (
generate_uuid,
clean_email,
clean_full_name,
hash_password
)
def test_generate_uuid():
# testing uuid length
assert len(generate_uuid()) == 36
# testing uuid format
assert len(generate_uuid().split('-')) == 5
def test_clean_email():
# testing clearning of spaces, tabs and newlines
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'
def test_clean_full_name():
# testing cleaning of spaces, tabs and newlines
assert clean_full_name('Shashank Kumar') == 'Shashank Kumar'
assert clean_full_name(' Shashank Kumar') == 'Shashank Kumar'
assert clean_full_name('Shashank Kumar ') == 'Shashank Kumar'
assert clean_full_name(' Shashank Kumar ') == 'Shashank Kumar'
import pytest
from modules.signup.exceptions import SignUpError
from modules.signup.validations import (
validate_email,
validate_first_pass,
validate_confirm_pass,
validate_full_name
)
def test_validate_email():
# Validate strip of whitespaces, newlines and tabs
assert validate_email('abc@shanky.xyz')
assert validate_email(' abc@shanky.xyz')
assert validate_email('abc@shanky.xyz ')
assert validate_email(' abc@shanky.xyz ')
# check for correct email format
with pytest.raises(SignUpError):
validate_email('abcshanky.xyz')
with pytest.raises(SignUpError):
validate_email('abc@!shanky.xyz')
# check for emply email address
with pytest.raises(SignUpError):
validate_email('')
with pytest.raises(SignUpError):
validate_email(' ')
def test_validate_first_pass():
# check for valid pass
assert validate_first_pass('mynewpass')
# check for empty pass
with pytest.raises(SignUpError):
validate_first_pass('')
def test_validate_confirm_pass():
# check for correct match
assert validate_confirm_pass('mynewpass', 'mynewpass')
# check for incorrect match
with pytest.raises(SignUpError):
validate_confirm_pass('mynewpass', 'mypass')
def test_validate_full_name():
# check for correct name format
assert validate_full_name('Shashank Kumar')
# check for empty name input
with pytest.raises(SignUpError):
validate_full_name('')
# check for invalid name format
with pytest.raises(SignUpError):
validate_full_name('Sanyam ! Khurana')
import pytest
class TestSample:
def test_sample(self):
assert type(True) != type('This is a sample test!')
<SignUp>:
BoxLayout:
size_hint: 0.35, 1
orientation: 'vertical'
Label:
text_size: self.size
size: self.texture_size
text: 'New'
halign: 'left'
valign: 'bottom'
font_name: 'ui/assets/fonts/Anton-Regular.ttf'
font_size: 50
padding_x: 10
Label:
text_size: self.size
size: self.texture_size
text: 'Contributor'
halign: 'left'
valign: 'middle'
font_name: 'ui/assets/fonts/Anton-Regular.ttf'
font_size: 50
padding_x: 10
Label:
text_size: self.size
size: self.texture_size
text: 'Wizard'
halign: 'left'
valign: 'top'
font_name: 'ui/assets/fonts/Anton-Regular.ttf'
font_size: 50
padding_x: 10
BoxLayout:
size_hint: 0.65, 1
orientation: 'vertical'
canvas.before:
Color:
rgba: 0.114, 0.306, 0.537, 1
Rectangle:
pos: self.pos
size: self.size
Label:
size_hint: 1, 0.16
text: 'Let\'s Set You Up'
font_name: 'ui/assets/fonts/VarelaRound-Regular.ttf'
font_size: 20
id: sign_up_top_label
BoxLayout:
size_hint: 1, 0.5
id: user_info_screen
BoxLayout:
size_hint: 0.45, 1
orientation: 'vertical'
Label:
text_size: self.size
size: self.texture_size
text: 'Email'
font_name: 'ui/assets/fonts/VarelaRound-Regular.ttf'
font_size: 20
halign: 'right'
id: email_label
BoxLayout:
size_hint: 1, 0.7
padding: 20, 0, 0, 0
TextInput:
multiline: False
font_name: 'ui/assets/fonts/VarelaRound-Regular.ttf'
font_size: 18
halign: 'right'
id: user_email
Label:
text_size: self.size
size: self.texture_size
text: 'Password'