diff --git a/qiita_db/support_files/populate_test_db.sql b/qiita_db/support_files/populate_test_db.sql index 12035c788..46c0aaed7 100644 --- a/qiita_db/support_files/populate_test_db.sql +++ b/qiita_db/support_files/populate_test_db.sql @@ -50,7 +50,7 @@ INSERT INTO qiita.user_level VALUES (7, 'wet-lab admin', 'Can access the private -- Data for Name: qiita_user; Type: TABLE DATA; Schema: qiita; Owner: antoniog -- -INSERT INTO qiita.qiita_user VALUES ('test@foo.bar', 4, '$2a$12$gnUi8Qg.0tvW243v889BhOBhWLIHyIJjjgaG6dxuRJkUM8nXG9Efe', 'Dude', 'Nowhere University', '123 fake st, Apt 0, Faketown, CO 80302', '111-222-3344', NULL, NULL, NULL, false); +INSERT INTO qiita.qiita_user VALUES ('test@foo.bar', 4, '$2a$12$gnUi8Qg.0tvW243v889BhOBhWLIHyIJjjgaG6dxuRJkUM8nXG9Efe', 'Dude', 'Nowhere University', '123 fake st, Apt 0, Faketown, CO 80302', '111-222-3344', NULL, NULL, NULL, false, '0000-0002-0975-9019', 'Rob-Knight', '_e3QL94AAAAJ'); INSERT INTO qiita.qiita_user VALUES ('shared@foo.bar', 4, '$2a$12$gnUi8Qg.0tvW243v889BhOBhWLIHyIJjjgaG6dxuRJkUM8nXG9Efe', 'Shared', 'Nowhere University', '123 fake st, Apt 0, Faketown, CO 80302', '111-222-3344', NULL, NULL, NULL, false); INSERT INTO qiita.qiita_user VALUES ('admin@foo.bar', 1, '$2a$12$gnUi8Qg.0tvW243v889BhOBhWLIHyIJjjgaG6dxuRJkUM8nXG9Efe', 'Admin', 'Owner University', '312 noname st, Apt K, Nonexistantown, CO 80302', '222-444-6789', NULL, NULL, NULL, false); INSERT INTO qiita.qiita_user VALUES ('demo@microbio.me', 4, '$2a$12$gnUi8Qg.0tvW243v889BhOBhWLIHyIJjjgaG6dxuRJkUM8nXG9Efe', 'Demo', 'Qiita Dev', '1345 Colorado Avenue', '303-492-1984', NULL, NULL, NULL, false); diff --git a/qiita_db/support_files/qiita-db-unpatched.sql b/qiita_db/support_files/qiita-db-unpatched.sql index 1ce86de39..a61b4645d 100644 --- a/qiita_db/support_files/qiita-db-unpatched.sql +++ b/qiita_db/support_files/qiita-db-unpatched.sql @@ -1888,7 +1888,10 @@ CREATE TABLE qiita.qiita_user ( user_verify_code character varying, pass_reset_code character varying, pass_reset_timestamp timestamp without time zone, - receive_processing_job_emails boolean DEFAULT false + receive_processing_job_emails boolean DEFAULT false, + social_orcid character varying DEFAULT NULL, + social_researchgate character varying DEFAULT NULL, + social_googlescholar character varying DEFAULT NULL ); diff --git a/qiita_db/test/test_user.py b/qiita_db/test/test_user.py index 2dced88d9..666746c36 100644 --- a/qiita_db/test/test_user.py +++ b/qiita_db/test/test_user.py @@ -72,7 +72,10 @@ def setUp(self): 'pass_reset_code': None, 'pass_reset_timestamp': None, 'user_verify_code': None, - 'receive_processing_job_emails': True + 'receive_processing_job_emails': True, + 'social_orcid': None, + 'social_researchgate': None, + 'social_googlescholar': None } def tearDown(self): @@ -125,7 +128,10 @@ def test_create_user(self): 'address': None, 'user_level_id': 5, 'receive_processing_job_emails': False, - 'email': 'testcreateuser@test.bar'} + 'email': 'testcreateuser@test.bar', + 'social_orcid': None, + 'social_researchgate': None, + 'social_googlescholar': None} self._check_correct_info(obs, exp) # Make sure new system messages are linked to user @@ -162,7 +168,10 @@ def test_create_user_info(self): 'user_verify_code': '', 'user_level_id': 5, 'receive_processing_job_emails': True, - 'email': 'testcreateuserinfo@test.bar'} + 'email': 'testcreateuserinfo@test.bar', + 'social_orcid': None, + 'social_researchgate': None, + 'social_googlescholar': None} self._check_correct_info(obs, exp) def test_create_user_column_not_allowed(self): @@ -229,7 +238,10 @@ def test_get_info(self): 'pass_reset_timestamp': None, 'user_verify_code': None, 'receive_processing_job_emails': False, - 'phone': '222-444-6789' + 'phone': '222-444-6789', + 'social_orcid': None, + 'social_researchgate': None, + 'social_googlescholar': None } self.assertEqual(self.user.info, expinfo) diff --git a/qiita_db/test/test_util.py b/qiita_db/test/test_util.py index a8df7ed3c..26fff3004 100644 --- a/qiita_db/test/test_util.py +++ b/qiita_db/test/test_util.py @@ -92,7 +92,8 @@ def test_get_table_cols(self): obs = qdb.util.get_table_cols("qiita_user") exp = {"email", "user_level_id", "password", "name", "affiliation", "address", "phone", "user_verify_code", "pass_reset_code", - "pass_reset_timestamp", "receive_processing_job_emails"} + "pass_reset_timestamp", "receive_processing_job_emails", + "social_orcid", "social_researchgate", "social_googlescholar"} self.assertEqual(set(obs), exp) def test_exists_table(self): diff --git a/qiita_pet/handlers/user_handlers.py b/qiita_pet/handlers/user_handlers.py index 116c69432..d75316a80 100644 --- a/qiita_pet/handlers/user_handlers.py +++ b/qiita_pet/handlers/user_handlers.py @@ -6,8 +6,11 @@ # The full license is in the file LICENSE, distributed with this software. # ----------------------------------------------------------------------------- +import re + from tornado.web import authenticated, HTTPError from wtforms import Form, StringField, BooleanField, validators +from wtforms.validators import ValidationError from qiita_pet.handlers.base_handlers import BaseHandler from qiita_pet.handlers.api_proxy import user_jobs_get_req @@ -21,6 +24,157 @@ class UserProfile(Form): + def validate_general(value: str, infomsg: str, url_prefix: str): + """Validate basic user inputs, i.e. check for leading/trailing + whitespaces and leading URL prefix, like http://scholar.google.com/ + + Parameters + ---------- + value : str + The WTform user input string. + infomsg : str + An error message to inform the user how to extract the correct + value. + url_prefix : str + The URL prefix of the social network + + Returns + ------- + None in case of empty input, otherwise the input value + + Raises + ------ + ValidationError if + a) input has leading or trailing whitespaces + b) input starts with the given url_prefix + """ + if (value is None) or (value == ""): + # nothing to complain, as input is empty + return None + + if value != value.strip(): + raise ValidationError( + 'Please remove all leading and trailing whitespaces from your ' + 'input.
%s' % infomsg) + + if len(url_prefix) > 0: + isPrefix = re.search("^%s" % url_prefix, value) + if isPrefix is not None: + raise ValidationError( + 'Please remove the "%s" part from your input.
%s' % ( + isPrefix[0], infomsg)) + + # if there is still no error raised, we return the actual value of the + # user input + return value + + def validator_orcid_id(form: Form, field: StringField): + """A WTForm validator to check if user input follows ORCID syntax. + + Parameters + ---------- + form : wtforms.Form + The WTform form enclosing the user input field. + field : wtforms.StringField + The WTform user input field. + + Returns + ------- + True, if user input is OK. + + Raises + ------ + ValidationError if user input is not valid + """ + infomsg = ('Enter only your 16 digit numerical ORCID identifier, where' + ' every four digits are separated with a dash "-". An ' + 'example is: 0000-0002-0975-9019') + value = UserProfile.validate_general( + field.data, infomsg, 'https://orcid.org') + if value is None: + return True + + if re.search(r"^\d{4}-\d{4}-\d{4}-\d{4}$", value) is None: + raise ValidationError( + "Your input does not follow the required format.
%s" % + infomsg) + + def validator_gscholar_id(form, field): + """A WTForm validator to check if user input follows google scholar ID + syntax. + + Parameters + ---------- + form : wtforms.Form + The WTform form enclosing the user input field. + field : wtforms.StringField + The WTform user input field. + + Returns + ------- + True, if user input is OK. + + Raises + ------ + ValidationError if user input is not valid + """ + infomsg = ('To retrieve your google scholar ID, surf to your profile ' + 'and copy the URL in your browser. It might read like ' + 'https://scholar.google.com/citations?user=_e3QL94AAAAJ&' + 'hl=en
Ignore everything left of the "?". The right ' + 'part is a set of key=value pairs, separated by "&" ' + 'characters. Find the key "user=", the right part up to ' + 'the next "&" is your google scholar ID, in the example: ' + '"_e3QL94AAAAJ"') + # we need a regex here, since we don't know the TLD the user is + # presenting to us + value = UserProfile.validate_general( + field.data, infomsg, r'https://scholar.google.\w{1,3}/citations\?') + if value is None: + return True + + if '&' in value: + raise ValidationError( + 'Your input contains multiple key=value pairs (we found at ' + 'least one "&" character).
%s' % infomsg) + if 'user=' in value: + raise ValidationError( + 'Please remove the key "user" and the "=" character from ' + 'your input.
%s' % infomsg) + if value.startswith('='): + raise ValidationError( + 'Please remove leading "=" characters from your input.' + '
%s' % infomsg) + + def validator_rgate_id(form, field): + """A WTForm validator to check if user input follows ResearchGate + user names. + + Parameters + ---------- + form : wtforms.Form + The WTform form enclosing the user input field. + field : wtforms.StringField + The WTform user input field. + + Returns + ------- + True, if user input is OK. + + Raises + ------ + ValidationError if user input is not valid + """ + infomsg = ('To retrieve your ResearchGate ID, surf to your profile ' + 'and copy the URL in your browser. It might read like ' + 'https://www.researchgate.net/profile/Rob-Knight
' + 'Your ID is the part right of the last "/", in the example:' + ' "Rob-Knight"') + value = UserProfile.validate_general( + field.data, infomsg, 'https://www.researchgate.net/profile/') + if value is None: + return True + name = StringField("Name", [validators.required()]) affiliation = StringField("Affiliation") address = StringField("Address") @@ -28,6 +182,13 @@ class UserProfile(Form): receive_processing_job_emails = BooleanField( "Receive Processing Job Emails?") + social_orcid = StringField( + "ORCID", [validator_orcid_id], description="0000-0002-0975-9019") + social_googlescholar = StringField( + "Google Scholar", [validator_gscholar_id], description="_e3QL94AAAAJ") + social_researchgate = StringField( + "ResearchGate", [validator_rgate_id], description="Rob-Knight") + class UserProfileHandler(BaseHandler): """Displays user profile page and handles profile updates""" @@ -44,11 +205,11 @@ def post(self): msg = "" user = self.current_user action = self.get_argument("action") + form_data = UserProfile() if action == "profile": - # tuple of colmns available for profile + # tuple of columns available for profile # FORM INPUT NAMES MUST MATCH DB COLUMN NAMES not_str_fields = ('receive_processing_job_emails') - form_data = UserProfile() form_data.process(data=self.request.arguments) profile = {name: data[0].decode('ascii') if name not in not_str_fields else @@ -59,16 +220,19 @@ def post(self): for field in form_data: if field.name not in not_str_fields: field.data = field.data[0].decode('ascii') - try: - user.info = profile - msg = "Profile updated successfully" - except Exception as e: - msg = "ERROR: profile could not be updated" - LogEntry.create('Runtime', "Cound not update profile: %s" % - str(e), info={'User': user.id}) + if form_data.validate() is False: + msg = ("ERROR: profile could not be updated" + " as some of your above inputs must be corrected.") + else: + try: + user.info = profile + msg = "Profile updated successfully" + except Exception as e: + msg = "ERROR: profile could not be updated" + LogEntry.create('Runtime', "Cound not update profile: %s" % + str(e), info={'User': user.id}) elif action == "password": - form_data = UserProfile() form_data.process(data=user.info) oldpass = self.get_argument("oldpass") newpass = self.get_argument("newpass") diff --git a/qiita_pet/static/img/logo_social_googlescholar.png b/qiita_pet/static/img/logo_social_googlescholar.png new file mode 100644 index 000000000..e9bd65a3a Binary files /dev/null and b/qiita_pet/static/img/logo_social_googlescholar.png differ diff --git a/qiita_pet/static/img/logo_social_orcid.png b/qiita_pet/static/img/logo_social_orcid.png new file mode 100644 index 000000000..5f4a5352a Binary files /dev/null and b/qiita_pet/static/img/logo_social_orcid.png differ diff --git a/qiita_pet/static/img/logo_social_researchgate.png b/qiita_pet/static/img/logo_social_researchgate.png new file mode 100644 index 000000000..23c283b5d Binary files /dev/null and b/qiita_pet/static/img/logo_social_researchgate.png differ diff --git a/qiita_pet/templates/user_profile.html b/qiita_pet/templates/user_profile.html index b83efffad..66da290d9 100644 --- a/qiita_pet/templates/user_profile.html +++ b/qiita_pet/templates/user_profile.html @@ -14,9 +14,17 @@

User Information

{% for form_item in profile %} -
- - {% raw form_item(class_='form-control') %} +
+
+ {% if form_item.id.startswith('social_') %} + + {% end %} + {% raw form_item.label %} +
+ {% raw form_item(class_='form-control', placeholder=form_item.description) %} + {% if form_item.errors %} +
{% for e in form_item.errors %}{%raw e%}
{% end %}
+ {% end %}
{% end %}
{{msg}}
diff --git a/qiita_pet/test/test_user_handlers.py b/qiita_pet/test/test_user_handlers.py index b0724eba6..42fd46d8a 100644 --- a/qiita_pet/test/test_user_handlers.py +++ b/qiita_pet/test/test_user_handlers.py @@ -7,8 +7,11 @@ # ----------------------------------------------------------------------------- from unittest import main +from wtforms.validators import ValidationError +from wtforms import StringField from qiita_pet.test.tornado_test_base import TestHandlerBase +from qiita_pet.handlers.user_handlers import UserProfile class TestUserProfile(TestHandlerBase): @@ -17,7 +20,6 @@ class TestUserProfile(TestHandlerBase): class TestUserProfileHandler(TestHandlerBase): - def test_get(self): response = self.get('/profile/') self.assertEqual(response.code, 200) @@ -37,10 +39,84 @@ def test_post_profile(self): 'affiliation': ['NEWNAME'], 'address': ['ADDRESS'], 'name': ['TESTDUDE'], - 'phone': ['111-222-3333']} + 'phone': ['111-222-3333'], + 'social_orcid': [''], + 'social_googlescholar': [''], + 'social_researchgate': ['']} response = self.post('/profile/', post_args) self.assertEqual(response.code, 200) + def test_validators_social(self): + # None or empty should be valid + obs = UserProfile.validate_general(None, "", "") + self.assertEqual(obs, None) + obs = UserProfile.validate_general("", "", "") + self.assertEqual(obs, None) + + # having white spaces should raise errors + with self.assertRaises(ValidationError): + obs = UserProfile.validate_general(" infix", "", "") + with self.assertRaises(ValidationError): + obs = UserProfile.validate_general("infix ", "", "") + with self.assertRaises(ValidationError): + obs = UserProfile.validate_general(" infix ", "", "") + obs = UserProfile.validate_general("infix", "", "") + self.assertEqual(obs, 'infix') + + with self.assertRaises(ValidationError): + obs = UserProfile.validate_general( + "http://kurt.com/id1234", "msg", r"http://kurt.\w{1,3}/") + + def test_validator_orcid_id(self): + field = StringField("testfield") + + field.data = "0000-0002-0975-9019" + obs = UserProfile.validator_orcid_id(None, field) + self.assertEqual(obs, None) + + field.data = "https://orcid.org/0000-0002-0975-9019" + with self.assertRaises(ValidationError): + obs = UserProfile.validator_orcid_id(None, field) + + field.data = "wrong" + with self.assertRaises(ValidationError): + obs = UserProfile.validator_orcid_id(None, field) + + def test_validator_gscholar_id(self): + field = StringField("testfield") + + field.data = "_e3QL94AAAAJ" + obs = UserProfile.validator_gscholar_id(None, field) + self.assertEqual(obs, None) + + field.data = ('https://scholar.google.com/citations?user=_e3QL94AAAAJ&' + 'hl=en') + with self.assertRaises(ValidationError): + obs = UserProfile.validator_gscholar_id(None, field) + + field.data = 'user=_e3QL94AAAAJ&hl=en' + with self.assertRaises(ValidationError): + obs = UserProfile.validator_gscholar_id(None, field) + + field.data = 'user=_e3QL94AAAAJ' + with self.assertRaises(ValidationError): + obs = UserProfile.validator_gscholar_id(None, field) + + field.data = '=_e3QL94AAAAJ' + with self.assertRaises(ValidationError): + obs = UserProfile.validator_gscholar_id(None, field) + + def test_validator_rgate_id(self): + field = StringField("testfield") + + field.data = "Rob-Knight" + obs = UserProfile.validator_rgate_id(None, field) + self.assertEqual(obs, None) + + field.data = 'https://www.researchgate.net/profile/Rob-Knight' + with self.assertRaises(ValidationError): + obs = UserProfile.validator_rgate_id(None, field) + class TestUserJobsHandler(TestHandlerBase): def test_get(self):