Skip to content

Commit

Permalink
Add social handles (qiita-spots#3412)
Browse files Browse the repository at this point in the history
* user profile can now also store social handles from google scholar, ORCID and ResearchGate

* set DEFAULT NULL to new qiita.qiita_user columns

* extending existing test

* extending tests

* add tests for validator functions for WTform StringFields

* operate on str instead of field.data
  • Loading branch information
sjanssen2 authored Jun 5, 2024
1 parent ffe1ec8 commit 9de3f38
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 22 deletions.
2 changes: 1 addition & 1 deletion qiita_db/support_files/populate_test_db.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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 ('[email protected]', 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 ('[email protected]', 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 ('[email protected]', 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 ('[email protected]', 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 ('[email protected]', 4, '$2a$12$gnUi8Qg.0tvW243v889BhOBhWLIHyIJjjgaG6dxuRJkUM8nXG9Efe', 'Demo', 'Qiita Dev', '1345 Colorado Avenue', '303-492-1984', NULL, NULL, NULL, false);
Expand Down
5 changes: 4 additions & 1 deletion qiita_db/support_files/qiita-db-unpatched.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
);


Expand Down
20 changes: 16 additions & 4 deletions qiita_db/test/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -125,7 +128,10 @@ def test_create_user(self):
'address': None,
'user_level_id': 5,
'receive_processing_job_emails': False,
'email': '[email protected]'}
'email': '[email protected]',
'social_orcid': None,
'social_researchgate': None,
'social_googlescholar': None}
self._check_correct_info(obs, exp)

# Make sure new system messages are linked to user
Expand Down Expand Up @@ -162,7 +168,10 @@ def test_create_user_info(self):
'user_verify_code': '',
'user_level_id': 5,
'receive_processing_job_emails': True,
'email': '[email protected]'}
'email': '[email protected]',
'social_orcid': None,
'social_researchgate': None,
'social_googlescholar': None}
self._check_correct_info(obs, exp)

def test_create_user_column_not_allowed(self):
Expand Down Expand Up @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion qiita_db/test/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
184 changes: 174 additions & 10 deletions qiita_pet/handlers/user_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,13 +24,171 @@


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.<br/>%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.<br/>%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.<br/>%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<br/>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).<br/>%s' % infomsg)
if 'user=' in value:
raise ValidationError(
'Please remove the key "user" and the "=" character from '
'your input.<br/>%s' % infomsg)
if value.startswith('='):
raise ValidationError(
'Please remove leading "=" characters from your input.'
'<br/>%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<br/>'
'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")
phone = StringField("Phone")
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"""
Expand All @@ -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
Expand All @@ -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")
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added qiita_pet/static/img/logo_social_orcid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added qiita_pet/static/img/logo_social_researchgate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 11 additions & 3 deletions qiita_pet/templates/user_profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ <h3>User Information</h3>
<form role="form" action="{% raw qiita_config.portal_dir %}/profile/" method="post">
<input type="hidden" name="action" value="profile">
{% for form_item in profile %}
<div class="form-group">
<label for="oldpass" class="col-sm-10 control-label">{% raw form_item.label %}</label>
{% raw form_item(class_='form-control') %}
<div class="form-group" style="display: flex; flex-direction: column;">
<div style="display: inline; padding-left: 1em;">
{% if form_item.id.startswith('social_') %}
<img style="height: 24px; padding-right: 5px;" src="{% raw qiita_config.portal_dir %}/static/img/logo_{% raw form_item.id %}.png"/>
{% end %}
{% raw form_item.label %}
</div>
{% raw form_item(class_='form-control', placeholder=form_item.description) %}
{% if form_item.errors %}
<div style='color: #ff0000; padding-left: 10px;'>{% for e in form_item.errors %}{%raw e%}<br/>{% end %}</div>
{% end %}
</div>
{% end %}
<div style="color:{% if msg.startswith('ERROR:') %}red{% else %}darkgreen{% end %};">{{msg}}</div>
Expand Down
Loading

0 comments on commit 9de3f38

Please sign in to comment.