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 ================= 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/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); }); } }, 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({ 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'