diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..542978af --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ +## Purpose +Built announcements and users email lists to post important announcements and allow users to communicate with each other. + +## Approach +Invoke GNU Mailman3 REST API. + +#### Open Questions and Pre-Merge TODOs +- [ ] Merge [validates email address by sending a confirmation email to users](https://github.com/Murali-group/GraphSpace/pull/368) +- [ ] [Install and Configure Mailman3 Suite for GraphSpace](https://github.com/Murali-group/GraphSpace/wiki/Install-and-Configure-Mailman3-Suite-for-GraphSpace-(Ubuntu-16.04,-PostgreSQL,-Apache2,-Postfix)) +- [ ] Create the email lists of GraphSpace and add them to settings file +- [ ] Update the database. + +## Learning +- [Mailman 3 suite documents](http://docs.mailman3.org/en/latest/) + +- [Install and Configure Mailman3 Suite for GraphSpace](https://github.com/Murali-group/GraphSpace/wiki/Install-and-Configure-Mailman3-Suite-for-GraphSpace-(Ubuntu-16.04,-PostgreSQL,-Apache2,-Postfix)) + + +#### Blog Posts +- [How to Pull Request](https://github.com/flexyford/pull-request) Github Repo with Learning focused Pull Request Template. diff --git a/applications/home/views.py b/applications/home/views.py index d9ddf1a0..02a378a9 100644 --- a/applications/home/views.py +++ b/applications/home/views.py @@ -6,7 +6,8 @@ from django.template import RequestContext from graphspace.utils import * from graphspace.exceptions import * - +from graphspace.utils import generate_uid +from django.conf import settings 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: @@ -251,21 +254,55 @@ def register(request): if 'user_id' in request_body and 'password' in request_body: # RegisterForm is bound to POST data register_form = RegisterForm(request_body) + if register_form.is_valid(): + token = generate_uid() + email_list_announcement = request_body['email_list_announcement'] + email_list_user = request_body['email_list_user'] 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 - - return HttpResponse(json.dumps(json_success_response(200, message='Registered!')), - content_type="application/json") + password=register_form.cleaned_data['password'], user_account_status=0, email_confirmation_code=token, + email_list_announcement=email_list_announcement, email_list_user=email_list_user) + + users.send_confirmation_email(request, request_body['user_id'], token, email_list_announcement, email_list_user) + 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": } + """ + + context = RequestContext(request) # Checkout base.py file to see what context processors are being applied here. + + 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, email=user.email, email_list_announcement=user.email_list_announcement, email_list_user=user.email_list_user) + if user is not None: + request.session['uid'] = user.email + request.session['admin'] = user.is_admin + request.session['email_list_announcement'] = user.email_list_announcement + request.session['email_list_user'] = user.email_list_user + announcement_list_message = 'announcements email list for GraphSpace ' + '(' + settings.ANNOUNCEMENTS_LIST + ')' if user.email_list_announcement == 1 else '' + user_list_message = 'users email list for GraphSpace ' + '(' + settings.USERS_LIST + ')' if user.email_list_user == 1 else '' + comma = ', ' if user.email_list_announcement == 1 and user.email_list_user == 1 else '' + context["success_message"] = 'Thank you! Your account has been activated successfully. You will also receive email confirmation(s) for the following email list(s): ' + announcement_list_message + comma + user_list_message +'.' + + return render(request, 'home/index.html', context) + 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. @@ -290,4 +327,4 @@ def images(request, query): :param request: HTTP GET Request """ - return redirect('/static' + request.path) \ No newline at end of file + return redirect('/static' + request.path) diff --git a/applications/users/controllers.py b/applications/users/controllers.py index be2fe623..2595be4b 100644 --- a/applications/users/controllers.py +++ b/applications/users/controllers.py @@ -8,6 +8,13 @@ from graphspace.exceptions import BadRequest, ErrorCodes from graphspace.utils import generate_uid +# The mailmanclient library provides official Python bindings for the GNU Mailman 3 REST API. +from mailmanclient import Client + +# In order to talk to Mailman, the engine's REST server must be running. +# Begin by instantiating a client object to access the root of the REST hierarchy, providing it the base URL, user name and password (for Basic Auth) +client = Client('http://localhost:8001/3.1', 'restadmin', 'restpass') + # import the logging library import logging @@ -30,13 +37,16 @@ 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, + 'email_list_announcement': user.email_list_announcement, + 'email_list_user': user.email_list_user } 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, email_list_announcement=None, email_list_user=None): user = {} if email is not None: user['email'] = email @@ -44,7 +54,18 @@ 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 + if email_list_announcement is not None: + user['email_list_announcement'] = email_list_announcement + if email_list_announcement == 1: + client_list_announcement = client.get_list(settings.ANNOUNCEMENTS_LIST) + client_list_announcement.subscribe(email, pre_verified=True, pre_confirmed=True) + if email_list_user is not None: + user['email_list_user'] = email_list_user + if email_list_user == 1: + client_list_user = client.get_list(settings.USERS_LIST) + client_list_user.subscribe(email, pre_verified=True, pre_confirmed=True) return db.update_user(request.db_session, id=user_id, updated_user=user) @@ -126,14 +147,16 @@ 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, email_list_announcement=None, email_list_user=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, email_list_announcement=email_list_announcement, + email_list_user=email_list_user) -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, email_list_announcement=None, email_list_user=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. @@ -142,11 +165,17 @@ def add_user(request, email=None, password="graphspace_public_user", is_admin=0) :param email: User ID of the user. Default value is dynamically generated user id. :param password: Password of the user. Default value is "public". :param admin: 1 if user has admin access else 0. Default value is 0. + :param user_account_status: 1 if the user has created account successfully else 0. + :param email_confirmation_code: confirmation code sent to email when the user creates account + :param email_list_announcement: 1 if the user has chosen to join GraphSpace announcement email list else 0 + :param email_list_user: 1 if the user has chosen to join GraphSpace users email list else 0 :return: User """ 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, email_list_announcement=email_list_announcement, + email_list_user=email_list_user) def is_member_of_group(request, username, group_id): @@ -290,6 +319,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 +344,12 @@ 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, email_list_announcement, email_list_user): + # 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..86140588 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, email_list_announcement, email_list_user): """ Add a new user. @@ -20,9 +20,13 @@ def add_user(db_session, email, password, is_admin): :param email: User ID of the user. :param password: Password of the user. :param admin: 1 if user has admin access else 0. + :param user_account_status: 1 if the user has created account successfully else 0. + :param email_confirmation_code: confirmation code sent to email when the user creates account + :param email_list_announcement: 1 if the user has chosen to join GraphSpace announcement email list else 0 + :param email_list_user: 1 if the user has chosen to join GraphSpace users email list 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, email_list_announcement=email_list_announcement, email_list_user=email_list_user) db_session.add(user) return user diff --git a/applications/users/models.py b/applications/users/models.py index 053fc6f1..ae3b82a1 100644 --- a/applications/users/models.py +++ b/applications/users/models.py @@ -17,13 +17,21 @@ class User(IDMixin, TimeStampMixin, Base): :param email: Email ID of the user. :param password: Password of the user. :param admin: 1 if the user has admin access else 0. + :param user_account_status: 1 if the user has created account successfully else 0. + :param email_confirmation_code: confirmation code sent to email when the user creates account + :param email_list_announcement: 1 if the user has chosen to join GraphSpace announcement email list else 0 + :param email_list_user: 1 if the user has chosen to join GraphSpace users email list else 0 """ __tablename__ = "user" 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) + email_list_announcement = Column(Integer, nullable=False, default=0) + email_list_user = Column(Integer, 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") owned_graphs = relationship("Graph", back_populates="owner", cascade="all, delete-orphan") diff --git a/docs/Email_Lists_for_GraphSpace.md b/docs/Email_Lists_for_GraphSpace.md new file mode 100644 index 00000000..7bbbf58e --- /dev/null +++ b/docs/Email_Lists_for_GraphSpace.md @@ -0,0 +1,39 @@ + +# Email Lists for GraphSpace + +[GraphSpace](http://graphspace.org) built announcements and users email lists to post important announcements and allow users to communicate with each other. + +## Users Email List for GraphSpace +The users list is meant for all users of GraphSpace to communicate with each other and with the GraphSpace administrators. Please use this list to pose questions about GraphSpace, discuss any problems you may have, give us feedback, request features etc. + +### Subscribe +To subscribe you can choose "Please add me to the users email list for GraphSpace" when create account, + +![Users Email List for GraphSpace](_static/images/email-list/gs-Screenshot-register-users-email-list.png.png) + +or send an email with 'subscribe' in the subject to [graphspace-users-join@graphspace.org](mailto:graphspace-users-join@graphspace.org). + +### Unsubscribe +To unsubscribe from users list, send an email with 'unsubscribe' in the subject to [graphspace-users-leave@graphspace.org](mailto:graphspace-users-leave@graphspace.org). + +### Post +To post to users list, send your email to [graphspace-users@graphspace.org](mailto:graphspace-users@graphspace.org). + +### Archives +You can access the [archives](http://email.graphspace.org/hyperkitty/list/graphspace-users@graphspace.org/) of the users list. + +## Announcements Email List for GraphSpace +The GraphSpace administrators will use this list to post important announcements about GraphSpace. Note that you will not be able to post to this mailing list. If you have a question about GraphSpace, please join the graphspace-users mailing list and post there. + +### Subscribe +To subscribe you can choose "Please add me to the announcements email list for GraphSpace" when create account, + +![Announcements Email List for GraphSpace](_static/images/email-list/gs-Screenshot-register-announcements-email-list.png) + +or send an email with 'subscribe' in the subject to [graphspace-announcements-join@graphspace.org](mailto:graphspace-announcements-join@graphspace.org). + +### Unsubscribe +To unsubscribe from announcements list, send an email with 'unsubscribe' in the subject to [graphspace-announcements-leave@graphspace.org](mailto:graphspace-announcements-leave@graphspace.org). + +### Archives +You can access the [archives](http://email.graphspace.org/hyperkitty/list/graphspace-announcements@graphspace.org/) of the announcements list. diff --git a/docs/_static/images/email-list/gs-Screenshot-register-announcements-email-list.png b/docs/_static/images/email-list/gs-Screenshot-register-announcements-email-list.png new file mode 100644 index 00000000..bd8d1ce8 Binary files /dev/null and b/docs/_static/images/email-list/gs-Screenshot-register-announcements-email-list.png differ diff --git a/docs/_static/images/email-list/gs-Screenshot-register-users-email-list.png b/docs/_static/images/email-list/gs-Screenshot-register-users-email-list.png new file mode 100644 index 00000000..fdf6a5c9 Binary files /dev/null and b/docs/_static/images/email-list/gs-Screenshot-register-users-email-list.png differ diff --git a/docs/index.rst b/docs/index.rst index 3d08fe4f..f661d7cb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,5 +23,6 @@ GraphSpace 2.0 User Manual Sharing_Graphs Editing_Layouts Organizing_Graphs_Using_Tags + Email_Lists_for_GraphSpace Programmers_Guide Release_Notes diff --git a/graphspace/settings/local.py b/graphspace/settings/local.py index 9b2fa36d..63ee42d1 100644 --- a/graphspace/settings/local.py +++ b/graphspace/settings/local.py @@ -22,6 +22,10 @@ # Path to GraphSPace PATH = "/Path_to_GraphSpace" +# Email lists of graphspace +ANNOUNCEMENTS_LIST = 'graphspace-announcements@graphspace.org' +USERS_LIST = 'graphspace-users@graphspace.org' + # SHOULD NEVER CHANGE THIS VALUE SECRET_KEY = 'this-is-a-secret-key-for-local-settings-only' @@ -45,4 +49,4 @@ 'HOST': 'localhost', 'PORT': '5432' } -} \ No newline at end of file +} diff --git a/requirements.txt b/requirements.txt index b75a2132..b952e136 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,3 +28,4 @@ elasticsearch-dsl>=5.0.0,<6.0.0 sphinx-rtd-theme sphinx recommonmark +mailmanclient==3.1.1 diff --git a/static/js/main.js b/static/js/main.js index aefeaf82..d7e3efab 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -61,6 +61,19 @@ var header = { var user_id = $("#user_id").val(); var password = $("#password").val(); var verify_password = $("#verify_password").val(); + var email_list_announcement = 0; + var email_list_user = 0; + var announcement_checked = document.getElementById("email_list_announcement"); + var user_checked = document.getElementById("email_list_user"); + + + if (announcement_checked.checked == true) { + email_list_announcement = 1; + } + + if (user_checked.checked == true) { + email_list_user = 1; + } if (!$("#user_id") || user_id.length == 0) { $.notify({ @@ -102,10 +115,17 @@ var header = { //POST Request to log in user jsonRequest('POST', "/register/", { "user_id": user_id, - "password": password + "password": password, + "email_list_announcement": email_list_announcement, + "email_list_user": email_list_user }, successCallback = function (response) { - window.location.reload(); + $('#signupModal').modal('hide'); + $.notify({ + message: response.Message + }, { + type: 'success' + }); }, errorCallback = function (response) { $.notify({ @@ -115,4 +135,4 @@ var header = { }); }); } -}; \ No newline at end of file +}; diff --git a/templates/home/index.html b/templates/home/index.html index 31f07b67..75b9dc3c 100644 --- a/templates/home/index.html +++ b/templates/home/index.html @@ -1,6 +1,26 @@ {% extends 'base.html' %} {% load staticfiles %} {% block content %} +{% if success_message != None %} + +{% endif %} +
-
\ No newline at end of file +