From 7299a70a9611dbd7917d307c4f02dbd27f610bee Mon Sep 17 00:00:00 2001 From: Hitesh Ramchandani Date: Wed, 31 Jan 2018 13:41:55 +0530 Subject: [PATCH 1/4] Disable buttons on submit and give proper error message. The buttons were disabled on 'Create Group' and 'Update Group'. Also the error message was changed to 'Group name already exists!' fixes #323 --- static/js/groups_page.js | 52 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/static/js/groups_page.js b/static/js/groups_page.js index be5f3c6e..b7e88453 100644 --- a/static/js/groups_page.js +++ b/static/js/groups_page.js @@ -72,6 +72,8 @@ var groupsPage = { submit: function (e) { e.preventDefault(); + $('#CreateGroupBtn').attr('disabled', true); + var group = { "name": $("#GroupNameInput").val() == "" ? undefined : $("#GroupNameInput").val(), "description": $("#GroupDescriptionInput").val() == "" ? undefined : $("#GroupDescriptionInput").val(), @@ -79,7 +81,13 @@ var groupsPage = { }; if (!group['name']) { - return alert("Please enter in a valid group name!"); + $('#CreateGroupBtn').attr('disabled', false); + $.notify({ + message: 'Please enter in a valid group name!' + }, { + type: 'warning' + }); + return; } apis.groups.add(group, @@ -89,7 +97,21 @@ var groupsPage = { }, errorCallback = function (xhr, status, errorThrown) { // This method is called when error occurs while adding group. - alert(xhr.responseText); + if(xhr.responseJSON.error_message.includes('duplicate key')) { + $.notify({ + message: 'Group name ' + group['name'] + ' already exists!' + }, { + type: 'danger' + }); + } + else { + $.notify({ + message: xhr.responseText + }, { + type: 'danger' + }); + } + $('#CreateGroupBtn').attr('disabled', false); }); } }, @@ -282,13 +304,21 @@ var groupPage = { submit: function (e) { e.preventDefault(); + $('#UpdateGroupBtn').attr('disabled', true); + var group = { "name": $("#GroupNameInput").val() == "" ? undefined : $("#GroupNameInput").val(), "description": $("#GroupDescriptionInput").val() == "" ? undefined : $("#GroupDescriptionInput").val() }; if (!group['name']) { - return alert("Please enter in a valid group name!"); + $('#UpdateGroupBtn').attr('disabled', false); + $.notify({ + message: 'Please enter in a valid group name!' + }, { + type: 'warning' + }); + return } apis.groups.update($('#GroupID').val(), group, @@ -298,7 +328,21 @@ var groupPage = { }, errorCallback = function (xhr, status, errorThrown) { // This method is called when error occurs while updating group. - alert(xhr.responseText); + if(xhr.responseJSON.error_message.includes('duplicate key')) { + $.notify({ + message: 'Group name ' + group['name'] + ' already exists!' + }, { + type: 'danger' + }); + } + else { + $.notify({ + message: xhr.responseText + }, { + type: 'danger' + }); + } + $('#UpdateGroupBtn').attr('disabled', false); }); } }, From 1b4abbf75b2d2efd97e4881e6b982eb43c0ea7ed Mon Sep 17 00:00:00 2001 From: lohani2280 Date: Mon, 5 Feb 2018 20:45:03 +0530 Subject: [PATCH 2/4] Added the name of the groups with which a graph is shared to the "Graph Information" tab. Refers https://github.com/Murali-group/GraphSpace/issues/277 --- templates/graph/graph_details_tab.html | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/templates/graph/graph_details_tab.html b/templates/graph/graph_details_tab.html index de38caa3..ee24f498 100644 --- a/templates/graph/graph_details_tab.html +++ b/templates/graph/graph_details_tab.html @@ -28,6 +28,23 @@ {% endfor %} + + + + + + {% if shared_groups %} + {% for k in shared_groups %} + + + + {% endfor %} + {% else %} + + + + {% endif %} +
Shared with groups
{{k.name|safe}}
'The graph is not shared with any groups'
From bdbb7f6ff28cf1388f1cdbc9cf8453cab402a45c Mon Sep 17 00:00:00 2001 From: lohani2280 Date: Thu, 15 Feb 2018 12:18:25 +0530 Subject: [PATCH 3/4] README.md: Added GraphSpace documentation link Fixes https://github.com/Murali-group/GraphSpace/issues/352 --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 7058d37a..58888ee8 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,12 @@ WSGIScriptAlias / /path_to_GraphSpace/graphspace/wsgi.py Refer to https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/modwsgi/ if any problems occur with the setup. +Documentation +================= + +GraphSpace has extensive documentation on the [user interface](http://docs.graphspace.org/en/latest/Quick_Tour_of_GraphSpace.html#welcome-screen), the [REST API](http://docs.graphspace.org/en/latest/Programmers_Guide.html#graphspace-rest-api) and a [Python package for programmatic interaction](http://manual.graphspace.org/projects/graphspace-python/en/latest/tutorial/index.html). + + Contributing ================= From aaa91e93503c10d2367ff3205225b5f265be5c5d Mon Sep 17 00:00:00 2001 From: lohani2280 Date: Sat, 24 Feb 2018 02:13:48 +0530 Subject: [PATCH 4/4] In order to validate the email address of the user provided during registration, a confirmation email with activation link is sent to the email address and when the user clicks the link, the email address is verified and the account gets activated. fixes https://github.com/Murali-group/GraphSpace/issues/360 --- applications/home/urls.py | 2 ++ applications/home/views.py | 39 +++++++++++++++++++++++----- applications/users/controllers.py | 28 +++++++++++++++----- applications/users/dal.py | 14 ++++++++-- applications/users/models.py | 2 ++ graphspace/exceptions/error_codes.py | 1 + static/js/main.js | 7 ++++- 7 files changed, 77 insertions(+), 16 deletions(-) diff --git a/applications/home/urls.py b/applications/home/urls.py index 2cabddce..f78f821e 100644 --- a/applications/home/urls.py +++ b/applications/home/urls.py @@ -14,4 +14,6 @@ url(r'^about_us/$', views.about_us_page, name='about_us'), url(r'^forgot_password/$', views.forgot_password_page, name='forgot_password'), url(r'^reset_password/$', views.reset_password_page, name='reset_password'), + url(r'^activate_account/$', views.activate_account_page, name='activate_account'), + ] diff --git a/applications/home/views.py b/applications/home/views.py index d9ddf1a0..cd3448cb 100644 --- a/applications/home/views.py +++ b/applications/home/views.py @@ -6,6 +6,7 @@ from django.template import RequestContext from graphspace.utils import * from graphspace.exceptions import * +from graphspace.utils import generate_uid def home_page(request): @@ -225,12 +226,14 @@ def login(request): request_body = json.loads(request.body) user = users.authenticate_user(request, username=request_body['user_id'], password=request_body['pw']) - if user is not None: + if user is not None and user['user_account_status'] == 1: request.session['uid'] = user['user_id'] request.session['admin'] = user['admin'] return HttpResponse( json.dumps(json_success_response(200, message='%s, Welcome to GraphSpace!' % user['user_id'])), content_type="application/json") + elif user is not None and user['user_account_status'] is not 1: + raise ValidationError(request, ErrorCodes.Validation.UserUnVerified) else: raise ValidationError(request, ErrorCodes.Validation.UserPasswordMisMatch) else: @@ -252,20 +255,42 @@ def register(request): # RegisterForm is bound to POST data register_form = RegisterForm(request_body) if register_form.is_valid(): + token = generate_uid() user = users.register(request, username=register_form.cleaned_data['user_id'], - password=register_form.cleaned_data['password']) - if user is not None: - request.session['uid'] = user.email - request.session['admin'] = user.is_admin + password=register_form.cleaned_data['password'], user_account_status=0, email_confirmation_code=token) - return HttpResponse(json.dumps(json_success_response(200, message='Registered!')), - content_type="application/json") + users.send_confirmation_email(request, request_body['user_id'], token) + return HttpResponse(json.dumps(json_success_response(200, message='A verification link has been sent to your email account.'+ + 'Please click on the link to verify your email and continue '+ + 'the registration process.')), + content_type="application/json") else: raise BadRequest(request) else: raise MethodNotAllowed(request) # Handle other type of request methods like GET, PUT, UPDATE. +def activate_account_page(request): + """ + Activate a user account + + :param request: HTTP GET Request containing: + + {"activation_code": } + """ + + if 'GET' == request.method: + user = users.get_email_confirmation_code(request, request.GET.get('activation_code', None)) + users.update_user(request, user.id, user_account_status=1) + if user is not None: + request.session['uid'] = user.email + request.session['admin'] = user.is_admin + return HttpResponseRedirect('/') + + else: + raise MethodNotAllowed(request) # Handle other type of request methods like GET, PUT, UPDATE. + + def logout(request): """ Log the user out and display logout page. diff --git a/applications/users/controllers.py b/applications/users/controllers.py index be2fe623..447774d3 100644 --- a/applications/users/controllers.py +++ b/applications/users/controllers.py @@ -30,13 +30,14 @@ def authenticate_user(request, username=None, password=None): 'id': user.id, 'user_id': user.email, 'password': user.password, - 'admin': user.is_admin + 'admin': user.is_admin, + 'user_account_status':user.user_account_status } else: return None -def update_user(request, user_id, email=None, password=None, is_admin=None): +def update_user(request, user_id, email=None, password=None, is_admin=None, user_account_status=None): user = {} if email is not None: user['email'] = email @@ -44,6 +45,8 @@ def update_user(request, user_id, email=None, password=None, is_admin=None): user['password'] = bcrypt.hashpw(password, bcrypt.gensalt()) if is_admin is not None: user['is_admin'] = is_admin + if user_account_status is not None: + user['user_account_status'] = user_account_status return db.update_user(request.db_session, id=user_id, updated_user=user) @@ -126,14 +129,15 @@ def search_users(request, email=None, limit=20, offset=0, order='desc', sort='na return total, users -def register(request, username=None, password=None): +def register(request, username=None, password=None, user_account_status=None, email_confirmation_code=None): if db.get_user(request.db_session, username): raise BadRequest(request, error_code=ErrorCodes.Validation.UserAlreadyExists, args=username) - return add_user(request, email=username, password=password) + return add_user(request, email=username, password=password, user_account_status=user_account_status, + email_confirmation_code=email_confirmation_code) -def add_user(request, email=None, password="graphspace_public_user", is_admin=0): +def add_user(request, email=None, password="graphspace_public_user", is_admin=0, user_account_status=None, email_confirmation_code=None): """ Add a new user. If email and password is not passed, it will create a user with default values. By default a user has no admin access. @@ -146,7 +150,8 @@ def add_user(request, email=None, password="graphspace_public_user", is_admin=0) """ email = "public_user_%s@graphspace.com" % generate_uid(size=10) if email is None else email - return db.add_user(request.db_session, email=email, password=bcrypt.hashpw(password, bcrypt.gensalt()), is_admin=is_admin) + return db.add_user(request.db_session, email=email, password=bcrypt.hashpw(password, bcrypt.gensalt()), is_admin=is_admin, + user_account_status=user_account_status, email_confirmation_code=email_confirmation_code) def is_member_of_group(request, username, group_id): @@ -290,6 +295,9 @@ def delete_group_graph(request, group_id, graph_id): def get_password_reset_by_code(request, code): return db.get_password_reset_by_code(request.db_session, code) +def get_email_confirmation_code(request, code): + return db.get_email_confirmation_code(request.db_session, code) + def delete_password_reset_code(request, id): return db.delete_password_reset(request.db_session, id) @@ -312,3 +320,11 @@ def send_password_reset_email(request, password_reset_code): email_from = "GraphSpace Admin" return send_mail(mail_title, message, email_from, [password_reset_code.email], fail_silently=False) + +def send_confirmation_email(request, email, token): + # Construct email message + mail_title = 'Activate your account for GraphSpace!' + message = 'Please confirm your email address to complete the registration ' + settings.URL_PATH + 'activate_account/?activation_code=' + token + email_from = "GraphSpace Admin" + + return send_mail(mail_title, message, email_from, [email], fail_silently=False) diff --git a/applications/users/dal.py b/applications/users/dal.py index 66b19861..39814c9e 100644 --- a/applications/users/dal.py +++ b/applications/users/dal.py @@ -12,7 +12,7 @@ @with_session -def add_user(db_session, email, password, is_admin): +def add_user(db_session, email, password, is_admin, user_account_status, email_confirmation_code): """ Add a new user. @@ -22,7 +22,8 @@ def add_user(db_session, email, password, is_admin): :param admin: 1 if user has admin access else 0. :return: User """ - user = User(email=email, password=password, is_admin = is_admin) + user = User(email=email, password=password, is_admin = is_admin, + user_account_status=user_account_status, email_confirmation_code=email_confirmation_code) db_session.add(user) return user @@ -113,6 +114,15 @@ def get_password_reset_by_code(db_session, code): """ return db_session.query(PasswordResetCode).filter(PasswordResetCode.code == code).one_or_none() +@with_session +def get_email_confirmation_code(db_session, code): + """ + :param db_session: Database session. + :param code: PasswordReset code + :return: PasswordReset if email exists else None + """ + return db_session.query(User).filter(User.email_confirmation_code == code).one_or_none() + @with_session def update_password_reset(db_session, id, updated_password_reset): diff --git a/applications/users/models.py b/applications/users/models.py index 053fc6f1..ae194a9c 100644 --- a/applications/users/models.py +++ b/applications/users/models.py @@ -23,6 +23,8 @@ class User(IDMixin, TimeStampMixin, Base): email = Column(String, nullable=False, unique=True, index=True) password = Column(String, nullable=False) is_admin = Column(Integer, nullable=False, default=0) + user_account_status = Column(Integer, nullable=False, default=0) + email_confirmation_code = Column(String, nullable=False, default=0) password_reset_codes = relationship("PasswordResetCode", back_populates="user", cascade="all, delete-orphan") owned_groups = relationship("Group", back_populates="owner", cascade="all, delete-orphan") diff --git a/graphspace/exceptions/error_codes.py b/graphspace/exceptions/error_codes.py index e229e976..d75367c7 100644 --- a/graphspace/exceptions/error_codes.py +++ b/graphspace/exceptions/error_codes.py @@ -12,6 +12,7 @@ class Validation(object): UserPasswordMisMatch = (1003, "User/Password not recognized") UserNotAuthorized = (1004, "You are not authorized to access this resource, create an account and contact resource's owner for permission to access this resource.") UserNotAuthenticated = (1005, "User authentication failed") + UserUnVerified = (10015, "User not verified") # Graphs API IsPublicNotSet = (1006, "`is_public` is required to be set to True when `owner_email` and `member_email` are not provided.") diff --git a/static/js/main.js b/static/js/main.js index aefeaf82..3f132fd1 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -105,7 +105,12 @@ var header = { "password": password }, successCallback = function (response) { - window.location.reload(); + $('#signupModal').modal('hide'); + $.notify({ + message: response.Message + }, { + type: 'success' + }); }, errorCallback = function (response) { $.notify({