Skip to content

Commit 9de3f38

Browse files
authored
Add social handles (#3412)
* 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
1 parent ffe1ec8 commit 9de3f38

10 files changed

+286
-22
lines changed

Diff for: qiita_db/support_files/populate_test_db.sql

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ INSERT INTO qiita.user_level VALUES (7, 'wet-lab admin', 'Can access the private
5050
-- Data for Name: qiita_user; Type: TABLE DATA; Schema: qiita; Owner: antoniog
5151
--
5252

53-
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);
53+
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');
5454
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);
5555
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);
5656
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);

Diff for: qiita_db/support_files/qiita-db-unpatched.sql

+4-1
Original file line numberDiff line numberDiff line change
@@ -1888,7 +1888,10 @@ CREATE TABLE qiita.qiita_user (
18881888
user_verify_code character varying,
18891889
pass_reset_code character varying,
18901890
pass_reset_timestamp timestamp without time zone,
1891-
receive_processing_job_emails boolean DEFAULT false
1891+
receive_processing_job_emails boolean DEFAULT false,
1892+
social_orcid character varying DEFAULT NULL,
1893+
social_researchgate character varying DEFAULT NULL,
1894+
social_googlescholar character varying DEFAULT NULL
18921895
);
18931896

18941897

Diff for: qiita_db/test/test_user.py

+16-4
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ def setUp(self):
7272
'pass_reset_code': None,
7373
'pass_reset_timestamp': None,
7474
'user_verify_code': None,
75-
'receive_processing_job_emails': True
75+
'receive_processing_job_emails': True,
76+
'social_orcid': None,
77+
'social_researchgate': None,
78+
'social_googlescholar': None
7679
}
7780

7881
def tearDown(self):
@@ -125,7 +128,10 @@ def test_create_user(self):
125128
'address': None,
126129
'user_level_id': 5,
127130
'receive_processing_job_emails': False,
128-
'email': '[email protected]'}
131+
'email': '[email protected]',
132+
'social_orcid': None,
133+
'social_researchgate': None,
134+
'social_googlescholar': None}
129135
self._check_correct_info(obs, exp)
130136

131137
# Make sure new system messages are linked to user
@@ -162,7 +168,10 @@ def test_create_user_info(self):
162168
'user_verify_code': '',
163169
'user_level_id': 5,
164170
'receive_processing_job_emails': True,
165-
'email': '[email protected]'}
171+
'email': '[email protected]',
172+
'social_orcid': None,
173+
'social_researchgate': None,
174+
'social_googlescholar': None}
166175
self._check_correct_info(obs, exp)
167176

168177
def test_create_user_column_not_allowed(self):
@@ -229,7 +238,10 @@ def test_get_info(self):
229238
'pass_reset_timestamp': None,
230239
'user_verify_code': None,
231240
'receive_processing_job_emails': False,
232-
'phone': '222-444-6789'
241+
'phone': '222-444-6789',
242+
'social_orcid': None,
243+
'social_researchgate': None,
244+
'social_googlescholar': None
233245
}
234246
self.assertEqual(self.user.info, expinfo)
235247

Diff for: qiita_db/test/test_util.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ def test_get_table_cols(self):
9292
obs = qdb.util.get_table_cols("qiita_user")
9393
exp = {"email", "user_level_id", "password", "name", "affiliation",
9494
"address", "phone", "user_verify_code", "pass_reset_code",
95-
"pass_reset_timestamp", "receive_processing_job_emails"}
95+
"pass_reset_timestamp", "receive_processing_job_emails",
96+
"social_orcid", "social_researchgate", "social_googlescholar"}
9697
self.assertEqual(set(obs), exp)
9798

9899
def test_exists_table(self):

Diff for: qiita_pet/handlers/user_handlers.py

+174-10
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
# The full license is in the file LICENSE, distributed with this software.
77
# -----------------------------------------------------------------------------
88

9+
import re
10+
911
from tornado.web import authenticated, HTTPError
1012
from wtforms import Form, StringField, BooleanField, validators
13+
from wtforms.validators import ValidationError
1114

1215
from qiita_pet.handlers.base_handlers import BaseHandler
1316
from qiita_pet.handlers.api_proxy import user_jobs_get_req
@@ -21,13 +24,171 @@
2124

2225

2326
class UserProfile(Form):
27+
def validate_general(value: str, infomsg: str, url_prefix: str):
28+
"""Validate basic user inputs, i.e. check for leading/trailing
29+
whitespaces and leading URL prefix, like http://scholar.google.com/
30+
31+
Parameters
32+
----------
33+
value : str
34+
The WTform user input string.
35+
infomsg : str
36+
An error message to inform the user how to extract the correct
37+
value.
38+
url_prefix : str
39+
The URL prefix of the social network
40+
41+
Returns
42+
-------
43+
None in case of empty input, otherwise the input value
44+
45+
Raises
46+
------
47+
ValidationError if
48+
a) input has leading or trailing whitespaces
49+
b) input starts with the given url_prefix
50+
"""
51+
if (value is None) or (value == ""):
52+
# nothing to complain, as input is empty
53+
return None
54+
55+
if value != value.strip():
56+
raise ValidationError(
57+
'Please remove all leading and trailing whitespaces from your '
58+
'input.<br/>%s' % infomsg)
59+
60+
if len(url_prefix) > 0:
61+
isPrefix = re.search("^%s" % url_prefix, value)
62+
if isPrefix is not None:
63+
raise ValidationError(
64+
'Please remove the "%s" part from your input.<br/>%s' % (
65+
isPrefix[0], infomsg))
66+
67+
# if there is still no error raised, we return the actual value of the
68+
# user input
69+
return value
70+
71+
def validator_orcid_id(form: Form, field: StringField):
72+
"""A WTForm validator to check if user input follows ORCID syntax.
73+
74+
Parameters
75+
----------
76+
form : wtforms.Form
77+
The WTform form enclosing the user input field.
78+
field : wtforms.StringField
79+
The WTform user input field.
80+
81+
Returns
82+
-------
83+
True, if user input is OK.
84+
85+
Raises
86+
------
87+
ValidationError if user input is not valid
88+
"""
89+
infomsg = ('Enter only your 16 digit numerical ORCID identifier, where'
90+
' every four digits are separated with a dash "-". An '
91+
'example is: 0000-0002-0975-9019')
92+
value = UserProfile.validate_general(
93+
field.data, infomsg, 'https://orcid.org')
94+
if value is None:
95+
return True
96+
97+
if re.search(r"^\d{4}-\d{4}-\d{4}-\d{4}$", value) is None:
98+
raise ValidationError(
99+
"Your input does not follow the required format.<br/>%s" %
100+
infomsg)
101+
102+
def validator_gscholar_id(form, field):
103+
"""A WTForm validator to check if user input follows google scholar ID
104+
syntax.
105+
106+
Parameters
107+
----------
108+
form : wtforms.Form
109+
The WTform form enclosing the user input field.
110+
field : wtforms.StringField
111+
The WTform user input field.
112+
113+
Returns
114+
-------
115+
True, if user input is OK.
116+
117+
Raises
118+
------
119+
ValidationError if user input is not valid
120+
"""
121+
infomsg = ('To retrieve your google scholar ID, surf to your profile '
122+
'and copy the URL in your browser. It might read like '
123+
'https://scholar.google.com/citations?user=_e3QL94AAAAJ&'
124+
'hl=en<br/>Ignore everything left of the "?". The right '
125+
'part is a set of key=value pairs, separated by "&" '
126+
'characters. Find the key "user=", the right part up to '
127+
'the next "&" is your google scholar ID, in the example: '
128+
'"_e3QL94AAAAJ"')
129+
# we need a regex here, since we don't know the TLD the user is
130+
# presenting to us
131+
value = UserProfile.validate_general(
132+
field.data, infomsg, r'https://scholar.google.\w{1,3}/citations\?')
133+
if value is None:
134+
return True
135+
136+
if '&' in value:
137+
raise ValidationError(
138+
'Your input contains multiple key=value pairs (we found at '
139+
'least one "&" character).<br/>%s' % infomsg)
140+
if 'user=' in value:
141+
raise ValidationError(
142+
'Please remove the key "user" and the "=" character from '
143+
'your input.<br/>%s' % infomsg)
144+
if value.startswith('='):
145+
raise ValidationError(
146+
'Please remove leading "=" characters from your input.'
147+
'<br/>%s' % infomsg)
148+
149+
def validator_rgate_id(form, field):
150+
"""A WTForm validator to check if user input follows ResearchGate
151+
user names.
152+
153+
Parameters
154+
----------
155+
form : wtforms.Form
156+
The WTform form enclosing the user input field.
157+
field : wtforms.StringField
158+
The WTform user input field.
159+
160+
Returns
161+
-------
162+
True, if user input is OK.
163+
164+
Raises
165+
------
166+
ValidationError if user input is not valid
167+
"""
168+
infomsg = ('To retrieve your ResearchGate ID, surf to your profile '
169+
'and copy the URL in your browser. It might read like '
170+
'https://www.researchgate.net/profile/Rob-Knight<br/>'
171+
'Your ID is the part right of the last "/", in the example:'
172+
' "Rob-Knight"')
173+
value = UserProfile.validate_general(
174+
field.data, infomsg, 'https://www.researchgate.net/profile/')
175+
if value is None:
176+
return True
177+
24178
name = StringField("Name", [validators.required()])
25179
affiliation = StringField("Affiliation")
26180
address = StringField("Address")
27181
phone = StringField("Phone")
28182
receive_processing_job_emails = BooleanField(
29183
"Receive Processing Job Emails?")
30184

185+
social_orcid = StringField(
186+
"ORCID", [validator_orcid_id], description="0000-0002-0975-9019")
187+
social_googlescholar = StringField(
188+
"Google Scholar", [validator_gscholar_id], description="_e3QL94AAAAJ")
189+
social_researchgate = StringField(
190+
"ResearchGate", [validator_rgate_id], description="Rob-Knight")
191+
31192

32193
class UserProfileHandler(BaseHandler):
33194
"""Displays user profile page and handles profile updates"""
@@ -44,11 +205,11 @@ def post(self):
44205
msg = ""
45206
user = self.current_user
46207
action = self.get_argument("action")
208+
form_data = UserProfile()
47209
if action == "profile":
48-
# tuple of colmns available for profile
210+
# tuple of columns available for profile
49211
# FORM INPUT NAMES MUST MATCH DB COLUMN NAMES
50212
not_str_fields = ('receive_processing_job_emails')
51-
form_data = UserProfile()
52213
form_data.process(data=self.request.arguments)
53214
profile = {name: data[0].decode('ascii')
54215
if name not in not_str_fields else
@@ -59,16 +220,19 @@ def post(self):
59220
for field in form_data:
60221
if field.name not in not_str_fields:
61222
field.data = field.data[0].decode('ascii')
62-
try:
63-
user.info = profile
64-
msg = "Profile updated successfully"
65-
except Exception as e:
66-
msg = "ERROR: profile could not be updated"
67-
LogEntry.create('Runtime', "Cound not update profile: %s" %
68-
str(e), info={'User': user.id})
223+
if form_data.validate() is False:
224+
msg = ("ERROR: profile could not be updated"
225+
" as some of your above inputs must be corrected.")
226+
else:
227+
try:
228+
user.info = profile
229+
msg = "Profile updated successfully"
230+
except Exception as e:
231+
msg = "ERROR: profile could not be updated"
232+
LogEntry.create('Runtime', "Cound not update profile: %s" %
233+
str(e), info={'User': user.id})
69234

70235
elif action == "password":
71-
form_data = UserProfile()
72236
form_data.process(data=user.info)
73237
oldpass = self.get_argument("oldpass")
74238
newpass = self.get_argument("newpass")

Diff for: qiita_pet/static/img/logo_social_googlescholar.png

7.43 KB
Loading

Diff for: qiita_pet/static/img/logo_social_orcid.png

9.35 KB
Loading

Diff for: qiita_pet/static/img/logo_social_researchgate.png

1.29 KB
Loading

Diff for: qiita_pet/templates/user_profile.html

+11-3
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,17 @@ <h3>User Information</h3>
1414
<form role="form" action="{% raw qiita_config.portal_dir %}/profile/" method="post">
1515
<input type="hidden" name="action" value="profile">
1616
{% for form_item in profile %}
17-
<div class="form-group">
18-
<label for="oldpass" class="col-sm-10 control-label">{% raw form_item.label %}</label>
19-
{% raw form_item(class_='form-control') %}
17+
<div class="form-group" style="display: flex; flex-direction: column;">
18+
<div style="display: inline; padding-left: 1em;">
19+
{% if form_item.id.startswith('social_') %}
20+
<img style="height: 24px; padding-right: 5px;" src="{% raw qiita_config.portal_dir %}/static/img/logo_{% raw form_item.id %}.png"/>
21+
{% end %}
22+
{% raw form_item.label %}
23+
</div>
24+
{% raw form_item(class_='form-control', placeholder=form_item.description) %}
25+
{% if form_item.errors %}
26+
<div style='color: #ff0000; padding-left: 10px;'>{% for e in form_item.errors %}{%raw e%}<br/>{% end %}</div>
27+
{% end %}
2028
</div>
2129
{% end %}
2230
<div style="color:{% if msg.startswith('ERROR:') %}red{% else %}darkgreen{% end %};">{{msg}}</div>

0 commit comments

Comments
 (0)