From 57b00317168eb3a84c489c2613133170a191aef6 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 15 Oct 2013 02:50:25 -0500 Subject: * Redid mail sending service to set a ping showing it's alive * Redid server so it wouldn't start unless mail service recently pinged * Wrote a bunch of forgot password code * Refactored sending email about verification and forgotten passwords --- wqflask/secure_server.py | 40 ++++- wqflask/wqflask/send_mail.py | 39 +++-- .../wqflask/templates/email/forgot_password.txt | 5 + wqflask/wqflask/templates/email/verification.txt | 3 +- .../templates/new_security/forgot_password.html | 65 ++++++++ .../wqflask/templates/new_security/login_user.html | 55 ++++--- wqflask/wqflask/user_manager.py | 165 ++++++++++++--------- 7 files changed, 258 insertions(+), 114 deletions(-) create mode 100644 wqflask/wqflask/templates/email/forgot_password.txt create mode 100644 wqflask/wqflask/templates/new_security/forgot_password.html diff --git a/wqflask/secure_server.py b/wqflask/secure_server.py index a77abf7e..d5f1a291 100644 --- a/wqflask/secure_server.py +++ b/wqflask/secure_server.py @@ -1,12 +1,18 @@ from __future__ import absolute_import, division, print_function +import time +import sys + from wqflask import app from flask import Flask, render_template +import redis +Redis = redis.StrictRedis() + # Setup mail -from flask.ext.mail import Mail -mail = Mail(app) +#from flask.ext.mail import Mail +#mail = Mail(app) from wqflask.model import * @@ -33,8 +39,38 @@ app.wsgi_app = ProxyFix(app.wsgi_app) #print("app.config is:", app.config) + + +def check_send_mail_running(): + """Ensure send_mail.py is running before we start the site + + It would be really easy to accidentally run the site + without our mail program running + This will make sure our mail program is running...or at least recently run... + + """ + error_msg = "Make sure your are running send_mail.py" + send_mail_ping = Redis.get("send_mail:ping") + print("send_mail_ping is:", send_mail_ping) + if not send_mail_ping: + sys.exit(error_msg) + + last_ping = time.time() - float(send_mail_ping) + if not (0 < last_ping < 100): + sys.exit(error_msg) + + + print("send_mail.py seems to be running...") + + if __name__ == '__main__': #create_user() + + + + check_send_mail_running() + + app.run(host='0.0.0.0', port=app.config['SERVER_PORT'], use_debugger=False, diff --git a/wqflask/wqflask/send_mail.py b/wqflask/wqflask/send_mail.py index be51ad0d..bf5d0dd8 100644 --- a/wqflask/wqflask/send_mail.py +++ b/wqflask/wqflask/send_mail.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, division, print_function import datetime +import time import simplejson as json @@ -12,34 +13,40 @@ import mailer def timestamp(): ts = datetime.datetime.utcnow() return ts.isoformat() - + def main(): while True: - print("Waiting for message to show up in queue...") - msg = Redis.blpop("mail_queue") - - # Queue name is the first element, we want the second, which is the actual message - msg = msg[1] - - print("\nGot a msg in queue at {}: {}".format(timestamp(), msg)) - # Todo: Truncate mail_processed when it gets to long - Redis.rpush("mail_processed", msg) - process_message(msg) - + print("I'm alive!") + + # Set something so we know it's running (or at least been running recently) + Redis.setex("send_mail:ping", 300, time.time()) + + msg = Redis.blpop("mail_queue", 30) + + if msg: + # Queue name is the first element, we want the second, which is the actual message + msg = msg[1] + + print("\n\nGot a msg in queue at {}: {}".format(timestamp(), msg)) + # Todo: Truncate mail_processed when it gets to long + Redis.rpush("mail_processed", msg) + process_message(msg) + + def process_message(msg): msg = json.loads(msg) - + message = mailer.Message() message.From = msg['From'] message.To = msg['To'] message.Subject = msg['Subject'] message.Body = msg['Body'] - + sender = mailer.Mailer('localhost') sender.send(message) print("Sent message at {}: {}\n".format(timestamp(), msg)) - + if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/wqflask/wqflask/templates/email/forgot_password.txt b/wqflask/wqflask/templates/email/forgot_password.txt new file mode 100644 index 00000000..e7d1389b --- /dev/null +++ b/wqflask/wqflask/templates/email/forgot_password.txt @@ -0,0 +1,5 @@ +Sorry to hear you lost your GeneNetwork password. + +To reset your password please click the following link, or cut and paste it into your browser window: + +{{ url_for_hmac("password_reset", code = verification_code, _external=True )}} diff --git a/wqflask/wqflask/templates/email/verification.txt b/wqflask/wqflask/templates/email/verification.txt index 29229c68..76149a3a 100644 --- a/wqflask/wqflask/templates/email/verification.txt +++ b/wqflask/wqflask/templates/email/verification.txt @@ -4,5 +4,4 @@ We need to verify your email address. To do that please click the following link, or cut and paste it into your browser window: -{{ url_for_hmac("verify", code = verification_code, _external=True )}} - +{{ url_for_hmac("verify_email", code = verification_code, _external=True )}} diff --git a/wqflask/wqflask/templates/new_security/forgot_password.html b/wqflask/wqflask/templates/new_security/forgot_password.html new file mode 100644 index 00000000..39e51f96 --- /dev/null +++ b/wqflask/wqflask/templates/new_security/forgot_password.html @@ -0,0 +1,65 @@ +{% extends "base.html" %} +{% block title %}Forgot Password{% endblock %} +{% block content %} + + {{ header("Forgot Password", "Easily reset your password.") }} + +
+ + + +
+ +

Enter your email address

+ +
And we'll send you a link to reset your password
+ + + +
+
+ + +
+ +
+ +
+
+ + +
+
+ + + +
+
+ +
+ +
+ +
+
Has your email address changed?
+ + If you no longer use the email address connected to your account, you can contact us for assistance. + +
+ +
+
+
+ + + {% endblock %} + +{% block js %} + + + {% include "new_security/_scripts.html" %} + +{% endblock %} diff --git a/wqflask/wqflask/templates/new_security/login_user.html b/wqflask/wqflask/templates/new_security/login_user.html index f232ccbc..4e308c75 100644 --- a/wqflask/wqflask/templates/new_security/login_user.html +++ b/wqflask/wqflask/templates/new_security/login_user.html @@ -2,82 +2,81 @@ {% block title %}Register{% endblock %} {% block content %} - {{ header("Login", "Gain access to GeneNetwork.") }} + {{ header("Sign in", "Gain access to GeneNetwork") }}
- +
- +

Don't have an account?

- - + + Create a new account - - + +
- +

Already have an account?

- +
Sign in here
- - - + + +
- - + +
- + - - + +
- - + +
- +
- - + +
- +
- + {% endblock %} -{% block js %} +{% block js %} - + {% include "new_security/_scripts.html" %} {% endblock %} - diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index a2dff7f2..1df2c153 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -19,8 +19,11 @@ import base64 import simplejson as json -from redis import StrictRedis -Redis = StrictRedis() +from sqlalchemy import orm + +#from redis import StrictRedis +import redis +Redis = redis.StrictRedis() from flask import (Flask, g, render_template, url_for, request, make_response, @@ -53,7 +56,7 @@ def timestamp(): class UserSession(object): cookie_name = 'session_id' - + def __init__(self): cookie = request.cookies.get(self.cookie_name) if not cookie: @@ -70,13 +73,13 @@ class UserSession(object): self.record = Redis.hgetall(self.redis_key) print("record is:", self.record) self.logged_in = True - - + + def delete_session(self): # And more importantly delete the redis record Redis.delete(self.cookie_name) print("At end of delete_session") - + @app.before_request def before_request(): g.user_session = UserSession() @@ -105,22 +108,22 @@ class UserManager(object): print(" ID:", dataset.id) print(" Confidential:", dataset.check_confidentiality()) #print(" ---> self.datasets:", self.datasets) - + class RegisterUser(object): def __init__(self, kw): self.thank_you_mode = False self.errors = [] self.user = Bunch() - + self.user.email_address = kw.get('email_address', '').strip() if not (5 <= len(self.user.email_address) <= 50): self.errors.append('Email Address needs to be between 5 and 50 characters.') - + self.user.full_name = kw.get('full_name', '').strip() if not (5 <= len(self.user.full_name) <= 50): self.errors.append('Full Name needs to be between 5 and 50 characters.') - + self.user.organization = kw.get('organization', '').strip() if self.user.organization and not (5 <= len(self.user.organization) <= 50): self.errors.append('Organization needs to be empty or between 5 and 50 characters.') @@ -128,48 +131,49 @@ class RegisterUser(object): password = str(kw.get('password', '')) if not (6 <= len(password)): self.errors.append('Password needs to be at least 6 characters.') - + if kw.get('password_confirm') != password: self.errors.append("Passwords don't match.") - + if self.errors: return - + print("No errors!") - + self.set_password(password) - + self.user.registration_info = json.dumps(basic_info(), sort_keys=True) - + self.new_user = model.User(**self.user.__dict__) db_session.add(self.new_user) db_session.commit() - + print("Adding verification email to queue") - self.send_email_verification() + #self.send_email_verification() + VerificationEmail(self.new_user) print("Added verification email to queue") - + self.thank_you_mode = True - - + + def set_password(self, password): pwfields = Bunch() - + pwfields.algorithm = "pbkdf2" pwfields.hashfunc = "sha256" #hashfunc = getattr(hashlib, pwfields.hashfunc) - - # Encoding it to base64 makes storing it in json much easier + + # Encoding it to base64 makes storing it in json much easier pwfields.salt = base64.b64encode(os.urandom(32)) # https://forums.lastpass.com/viewtopic.php?t=84104 - pwfields.iterations = 100000 + pwfields.iterations = 100000 pwfields.keylength = 32 - + pwfields.created_ts = timestamp() # One more check on password length assert len(password) >= 6, "Password shouldn't be so short here" - + print("pwfields:", vars(pwfields)) print("locals:", locals()) @@ -178,31 +182,42 @@ class RegisterUser(object): pwfields.iterations, pwfields.keylength, pwfields.hashfunc) - + pwfields.password = enc_password.password pwfields.encrypt_time = enc_password.encrypt_time - + self.user.password = json.dumps(pwfields.__dict__, - sort_keys=True, + sort_keys=True, ) - - def send_email_verification(self): + +class VerificationEmail(object): + template_name = "email/verification.txt" + key_preface = "verification_code" + subject = "GeneNetwork email verification" + + def __init__(self, user): verification_code = str(uuid.uuid4()) - key = "verification_code:" + verification_code - - data = json.dumps(dict(id=self.new_user.id, + key = self.key_preface + "verification_code:" + verification_code + + data = json.dumps(dict(id=user.id, timestamp=timestamp()) ) - + Redis.set(key, data) two_days = 60 * 60 * 24 * 2 - Redis.expire(key, two_days) - to = self.user.email_address - subject = "GeneNetwork email verification" - body = render_template("email/verification.txt", + Redis.expire(key, two_days) + to = user.email_address + subject = self.subject + body = render_template(self.template_name, verification_code = verification_code) send_email(to, subject, body) +class ForgotPasswordEmail(VerificationEmail): + template_name = "email/forgot_password.txt" + key_preface = "forgot_password_code" + subject = "GeneNetwork password reset" + + class Password(object): def __init__(self, unencrypted_password, salt, iterations, keylength, hashfunc): hashfunc = getattr(hashlib, hashfunc) @@ -215,13 +230,14 @@ class Password(object): salt, iterations, keylength, hashfunc) self.encrypt_time = round(time.time() - start_time, 3) print("Creating password took:", self.encrypt_time) - - + + def basic_info(): return dict(timestamp = timestamp(), ip_address = request.remote_addr, user_agent = request.headers.get('User-Agent')) +@app.route("/manage/verify_email") def verify_email(): print("in verify_email request.url is:", request.url) verify_url_hmac(request.url) @@ -232,7 +248,7 @@ def verify_email(): user = model.User.query.get(data['id']) user.confirmed = json.dumps(basic_info(), sort_keys=True) db_session.commit() - + def login(): params = request.form if request.form else request.args print("in login params are:", params) @@ -250,10 +266,10 @@ def login(): print("\n\nComparing:\n{}\n{}\n".format(encrypted.password, pwfields.password)) valid = pbkdf2.safe_str_cmp(encrypted.password, pwfields.password) print("valid is:", valid) - + login_rec = model.Login(user) - - + + if valid: login_rec.successful = True login_rec.session_id = str(uuid.uuid4()) @@ -261,17 +277,17 @@ def login(): session_id_signature = actual_hmac_creation(login_rec.session_id) session_id_signed = login_rec.session_id + ":" + session_id_signature print("session_id_signed:", session_id_signed) - + session = dict(login_time = time.time(), user_id = user.id, user_email_address = user.email_address) - + flash("Thank you for logging in.", "alert-success") - + key = "session_id:" + login_rec.session_id print("Key when signing:", key) Redis.hmset(key, session) - + response = make_response(redirect(url_for('index_page'))) response.set_cookie(UserSession.cookie_name, session_id_signed) else: @@ -281,8 +297,8 @@ def login(): db_session.add(login_rec) db_session.commit() return response - -@app.route("/n/logout") + +@app.route("/n/logout") def logout(): print("Logging out...") UserSession().delete_session() @@ -291,15 +307,36 @@ def logout(): # Delete the cookie response.set_cookie(UserSession.cookie_name, '', expires=0) return response - - + + +@app.route("/n/forgot_password") +def forgot_password(): + return render_template("new_security/forgot_password.html") + +@app.route("/n/forgot_password_submit", methods=('POST',)) +def forgot_password_submit(): + params = request.form + email_address = params['email_address'] + try: + user = model.User.query.filter_by(email_address=email_address).one() + except orm.exc.NoResultFound: + flash("Couldn't find a user associated with the email address {}. Sorry.".format( + email_address)) + return redirect(url_for("login")) + ForgotPasswordEmail(user) + +@app.route("/n/password_reset") +def password_reset(): + pass + + ################################# Sign and unsign ##################################### def url_for_hmac(endpoint, **values): """Like url_for but adds an hmac at the end to insure the url hasn't been tampered with""" url = url_for(endpoint, **values) - + hm = actual_hmac_creation(url) if '?' in url: combiner = "&" @@ -323,10 +360,10 @@ def verify_url_hmac(url): hm = actual_hmac_creation(url) assert hm == hmac, "Unexpected url (stage 3)" - + def actual_hmac_creation(stringy): """Helper function to create the actual hmac""" - + secret = app.config['SECRET_HMAC_CODE'] hmaced = hmac.new(secret, stringy, hashlib.sha1) @@ -339,14 +376,14 @@ def actual_hmac_creation(stringy): app.jinja_env.globals.update(url_for_hmac=url_for_hmac) ####################################################################################### - + def send_email(to, subject, body): msg = json.dumps(dict(From="no-reply@genenetwork.org", To=to, Subject=subject, Body=body)) Redis.rpush("mail_queue", msg) - + #def combined_salt(user_salt): # """Combine the master salt with the user salt...we use two seperate salts so that if the database is compromised, the @@ -358,7 +395,7 @@ def send_email(to, subject, body): # for x, y in user_salt, secret_salt: # combined = combined + x + y # return combined - + class GroupsManager(object): @@ -374,9 +411,5 @@ class RolesManager(object): #class Password(object): # """To generate a master password: dd if=/dev/urandom bs=32 count=1 > master_salt""" -# -# master_salt = - - - - +# +# master_salt = -- cgit v1.2.3 From 612bc953cc82bee292a6a230b6604cc3070ea6d8 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 15 Oct 2013 16:47:08 -0500 Subject: Before getting rid of query_string in DecodeUser --- .../new_security/forgot_password_step2.html | 33 +++++++++ .../templates/new_security/password_reset.html | 78 ++++++++++++++++++++++ .../templates/new_security/register_user.html | 56 +++++++--------- .../wqflask/templates/new_security/registered.html | 11 ++- 4 files changed, 141 insertions(+), 37 deletions(-) create mode 100644 wqflask/wqflask/templates/new_security/forgot_password_step2.html create mode 100644 wqflask/wqflask/templates/new_security/password_reset.html diff --git a/wqflask/wqflask/templates/new_security/forgot_password_step2.html b/wqflask/wqflask/templates/new_security/forgot_password_step2.html new file mode 100644 index 00000000..1295e589 --- /dev/null +++ b/wqflask/wqflask/templates/new_security/forgot_password_step2.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block content %} +
+
+

Password Reset

+

+ Check your email. +

+
+
+ +
+ + +

You will receive an email with the subject "{{ subject }}".

+ +

You must click the link in the email to reset the password.

+ +

If you don't see the email, check your spam folder.

+
+ +{% endblock %} + +{% block js %} + + + {% include "new_security/_scripts.html" %} + + +{% endblock %} diff --git a/wqflask/wqflask/templates/new_security/password_reset.html b/wqflask/wqflask/templates/new_security/password_reset.html new file mode 100644 index 00000000..c66ddd07 --- /dev/null +++ b/wqflask/wqflask/templates/new_security/password_reset.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block content %} + + {{ header("Password Reset", "Create a new password.") }} + + +
+ + +
+ + +

Enter your new password

+ + + {% if errors %} +
+ Please note: +
    + {% for error in errors %} +
  • {{error}}
  • + {% endfor %} +
+
+ {% endif %} + +
+
+ + + +
+ +
+ +
+
+ + + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+ +{% endblock %} + +{% block js %} + + + {% include "new_security/_scripts.html" %} + + +{% endblock %} diff --git a/wqflask/wqflask/templates/new_security/register_user.html b/wqflask/wqflask/templates/new_security/register_user.html index 2a02e7ca..998d2a7b 100644 --- a/wqflask/wqflask/templates/new_security/register_user.html +++ b/wqflask/wqflask/templates/new_security/register_user.html @@ -1,36 +1,31 @@ {% extends "base.html" %} {% block title %}Register{% endblock %} {% block content %} -
-
-

Register

-

- It's easy and fast to make an account. -

-
-
+ + {{ header("Register", "It's fast and easy to make an account.") }} +
- +

Already have an account?

- - + + Sign in using existing account - - + +
- +

Don't have an account?

- +
Register here
- + {% if errors %} -
+
Please note:
    {% for error in errors %} @@ -39,11 +34,11 @@
{% endif %} - +
- +
@@ -51,7 +46,7 @@ data-trigger="change" data-required="true" data-type="email" data-maxlength="50">
- +
@@ -59,28 +54,28 @@ data-trigger="change" data-required="true" data-minlength="5" data-maxlength="50">
- +
- +
-
- +
+ - +
@@ -88,26 +83,25 @@ data-trigger="change" data-required="true" data-equalto="#password">
- +
- + - +
{% endblock %} -{% block js %} +{% block js %} - + {% include "new_security/_scripts.html" %} {% endblock %} - diff --git a/wqflask/wqflask/templates/new_security/registered.html b/wqflask/wqflask/templates/new_security/registered.html index 391a6044..49dc961f 100644 --- a/wqflask/wqflask/templates/new_security/registered.html +++ b/wqflask/wqflask/templates/new_security/registered.html @@ -15,20 +15,19 @@

One last step

-

You will receive an email with the subject "GeneNetwork email verification".

- +

You will receive an email with the subject "{{ subject }}".

+

You must click the link in the email to complete registration.

- +

If you don't see the email, check your spam folder.

{% endblock %} -{% block js %} +{% block js %} - + {% include "new_security/_scripts.html" %} {% endblock %} - -- cgit v1.2.3 From 5b7d03c4af45a6e9b556368746d3ff04b6942a57 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 15 Oct 2013 18:17:02 -0500 Subject: Password reset now works...but needs some error checking still --- .../templates/new_security/password_reset.html | 2 + wqflask/wqflask/user_manager.py | 191 ++++++++++++++++----- wqflask/wqflask/views.py | 64 +------ 3 files changed, 154 insertions(+), 103 deletions(-) diff --git a/wqflask/wqflask/templates/new_security/password_reset.html b/wqflask/wqflask/templates/new_security/password_reset.html index c66ddd07..cda1e477 100644 --- a/wqflask/wqflask/templates/new_security/password_reset.html +++ b/wqflask/wqflask/templates/new_security/password_reset.html @@ -29,8 +29,10 @@
+
+
diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index 1df2c153..766f49df 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -17,6 +17,8 @@ import hashlib import hmac import base64 +import urlparse + import simplejson as json from sqlalchemy import orm @@ -140,7 +142,7 @@ class RegisterUser(object): print("No errors!") - self.set_password(password) + set_password(password, user) self.user.registration_info = json.dumps(basic_info(), sort_keys=True) @@ -156,48 +158,49 @@ class RegisterUser(object): self.thank_you_mode = True - def set_password(self, password): - pwfields = Bunch() +def set_password(password, user): + pwfields = Bunch() + + pwfields.algorithm = "pbkdf2" + pwfields.hashfunc = "sha256" + #hashfunc = getattr(hashlib, pwfields.hashfunc) - pwfields.algorithm = "pbkdf2" - pwfields.hashfunc = "sha256" - #hashfunc = getattr(hashlib, pwfields.hashfunc) + # Encoding it to base64 makes storing it in json much easier + pwfields.salt = base64.b64encode(os.urandom(32)) - # Encoding it to base64 makes storing it in json much easier - pwfields.salt = base64.b64encode(os.urandom(32)) + # https://forums.lastpass.com/viewtopic.php?t=84104 + pwfields.iterations = 100000 + pwfields.keylength = 32 - # https://forums.lastpass.com/viewtopic.php?t=84104 - pwfields.iterations = 100000 - pwfields.keylength = 32 + pwfields.created_ts = timestamp() + # One more check on password length + assert len(password) >= 6, "Password shouldn't be so short here" - pwfields.created_ts = timestamp() - # One more check on password length - assert len(password) >= 6, "Password shouldn't be so short here" + print("pwfields:", vars(pwfields)) + print("locals:", locals()) - print("pwfields:", vars(pwfields)) - print("locals:", locals()) + enc_password = Password(password, + pwfields.salt, + pwfields.iterations, + pwfields.keylength, + pwfields.hashfunc) - enc_password = Password(password, - pwfields.salt, - pwfields.iterations, - pwfields.keylength, - pwfields.hashfunc) + pwfields.password = enc_password.password + pwfields.encrypt_time = enc_password.encrypt_time - pwfields.password = enc_password.password - pwfields.encrypt_time = enc_password.encrypt_time + user.password = json.dumps(pwfields.__dict__, + sort_keys=True, + ) - self.user.password = json.dumps(pwfields.__dict__, - sort_keys=True, - ) class VerificationEmail(object): template_name = "email/verification.txt" - key_preface = "verification_code" + key_prefix = "verification_code" subject = "GeneNetwork email verification" def __init__(self, user): verification_code = str(uuid.uuid4()) - key = self.key_preface + "verification_code:" + verification_code + key = self.key_prefix + ":" + verification_code data = json.dumps(dict(id=user.id, timestamp=timestamp()) @@ -214,7 +217,7 @@ class VerificationEmail(object): class ForgotPasswordEmail(VerificationEmail): template_name = "email/forgot_password.txt" - key_preface = "forgot_password_code" + key_prefix = "forgot_password_code" subject = "GeneNetwork password reset" @@ -239,16 +242,71 @@ def basic_info(): @app.route("/manage/verify_email") def verify_email(): - print("in verify_email request.url is:", request.url) - verify_url_hmac(request.url) - verification_code = request.args['code'] - data = Redis.get("verification_code:" + verification_code) - data = json.loads(data) - print("data is:", data) - user = model.User.query.get(data['id']) + user = DecodeUser(VerificationEmail.key_prefix).user user.confirmed = json.dumps(basic_info(), sort_keys=True) db_session.commit() +@app.route("/n/password_reset") +def password_reset(): + print("in password_reset request.url is:", request.url) + + # We do this mainly just to assert that it's in proper form for displaying next page + # Really not necessary but doesn't hurt + user_encode = DecodeUser(ForgotPasswordEmail.key_prefix).reencode_standalone() + + return render_template("new_security/password_reset.html", user_encode=user_encode) + +@app.route("/n/password_reset_step2", methods=('POST',)) +def password_reset_step2(): + print("in password_reset request.url is:", request.url) + + errors = [] + + user_encode = request.form['user_encode'] + verification_code, separator, hmac = user_encode.partition(':') + + hmac_verified = actual_hmac_creation(verification_code) + print("locals are:", locals()) + + + assert hmac == hmac_verified, "Someone has been naughty" + + user = DecodeUser.actual_get_user(ForgotPasswordEmail.key_prefix, verification_code) + print("user is:", user) + + password = request.form['password'] + + set_password(password, user) + db_session.commit() + + flash("Password changed successfully. You can now sign in.", "alert-info") + response = make_response(redirect(url_for('login'))) + + return response + +class DecodeUser(object): + + def __init__(self, code_prefix): + verify_url_hmac(request.url) + + #params = urlparse.parse_qs(url) + + self.verification_code = request.args['code'] + self.user = self.actual_get_user(code_prefix, self.verification_code) + + def reencode_standalone(self): + hmac = actual_hmac_creation(self.verification_code) + return self.verification_code + ":" + hmac + + @staticmethod + def actual_get_user(code_prefix, verification_code): + data = Redis.get(code_prefix + ":" + verification_code) + print("in get_coded_user, data is:", data) + data = json.loads(data) + print("data is:", data) + return model.User.query.get(data['id']) + +@app.route("/n/login", methods=('GET', 'POST')) def login(): params = request.form if request.form else request.args print("in login params are:", params) @@ -282,7 +340,7 @@ def login(): user_id = user.id, user_email_address = user.email_address) - flash("Thank you for logging in.", "alert-success") + flash("Thank you for logging in {}.".format(user.full_name), "alert-success") key = "session_id:" + login_rec.session_id print("Key when signing:", key) @@ -324,10 +382,63 @@ def forgot_password_submit(): email_address)) return redirect(url_for("login")) ForgotPasswordEmail(user) + return render_template("new_security/forgot_password_step2.html", + subject=ForgotPasswordEmail.subject) + + + +@app.route("/manage/users") +def manage_users(): + template_vars = UsersManager() + return render_template("admin/user_manager.html", **template_vars.__dict__) + +@app.route("/manage/user") +def manage_user(): + template_vars = UserManager(request.args) + return render_template("admin/ind_user_manager.html", **template_vars.__dict__) + +@app.route("/manage/groups") +def manage_groups(): + template_vars = GroupsManager(request.args) + return render_template("admin/group_manager.html", **template_vars.__dict__) + + +@app.route("/n/register", methods=('GET', 'POST')) +def register(): + params = None + errors = None + + #if request.form: + # params = request.form + #else: + # params = request.args + + params = request.form if request.form else request.args + + if params: + print("Attempting to register the user...") + result = RegisterUser(params) + errors = result.errors + + if result.thank_you_mode: + assert not errors, "Errors while in thank you mode? That seems wrong..." + return render_template("new_security/registered.html", + subject=VerificationEmail.subject) + + return render_template("new_security/register_user.html", values=params, errors=errors) + + + + +#@app.route("/n/login", methods=('GET', 'POST')) +#def login(): +# return user_manager.login() +# +#@app.route("/manage/verify") +#def verify(): +# user_manager.verify_email() +# return render_template("new_security/verified.html") -@app.route("/n/password_reset") -def password_reset(): - pass ################################# Sign and unsign ##################################### diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py index deccf459..a060ba3c 100644 --- a/wqflask/wqflask/views.py +++ b/wqflask/wqflask/views.py @@ -51,7 +51,7 @@ from wqflask import user_manager @app.before_request def connect_db(): g.db = sqlalchemy.create_engine(app.config['DB_URI']) - + #@app.before_request #def trace_it(): # from wqflask import tracer @@ -277,68 +277,6 @@ def get_temp_data(): ################################################################################################### -@app.route("/manage/users") -def manage_users(): - template_vars = user_manager.UsersManager() - return render_template("admin/user_manager.html", **template_vars.__dict__) - -@app.route("/manage/user") -def manage_user(): - template_vars = user_manager.UserManager(request.args) - return render_template("admin/ind_user_manager.html", **template_vars.__dict__) - -@app.route("/manage/groups") -def manage_groups(): - template_vars = user_manager.GroupsManager(request.args) - return render_template("admin/group_manager.html", **template_vars.__dict__) - - -@app.route("/n/register", methods=('GET', 'POST')) -def register(): - params = None - errors = None - - #if request.form: - # params = request.form - #else: - # params = request.args - - params = request.form if request.form else request.args - - if params: - print("Attempting to register the user...") - result = user_manager.RegisterUser(params) - errors = result.errors - - if result.thank_you_mode: - assert not errors, "Errors while in thank you mode? That seems wrong..." - return render_template("new_security/registered.html") - - return render_template("new_security/register_user.html", values=params, errors=errors) - -#@app.route("/n/register_submit", methods=('POST',)) -#def register_submit(): -# print("request.args are: ", request.args) -# result = user_manager.RegisterUser(request.form) -# if result.errors: -# print("Redirecting") -# # 307 preserves the post on the redirect (maybe) -# errors = result.errors -# #errors = json.dumps(errors) -# print("request.args are: ", request.args) -# return render_template("new_security/register_user.html", errors=errors, values=request.form) -# #return redirect(url_for('new_register', errors=errors), code=307) - - -@app.route("/n/login", methods=('GET', 'POST')) -def login(): - return user_manager.login() - -@app.route("/manage/verify") -def verify(): - user_manager.verify_email() - return render_template("new_security/verified.html") - ########################################################################## -- cgit v1.2.3 From ad94850e0916346af8cdb72c77f4ef7889d6ee95 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 16 Oct 2013 17:49:38 -0500 Subject: security code cleanup --- .../new_security/forgot_password_step2.html | 11 +--- .../wqflask/templates/new_security/registered.html | 13 +--- .../wqflask/templates/new_security/thank_you.html | 18 ++---- .../wqflask/templates/new_security/verified.html | 32 --------- wqflask/wqflask/templates/security/_macros.html | 39 ----------- wqflask/wqflask/templates/security/_menu.html | 15 ----- wqflask/wqflask/templates/security/_messages.html | 9 --- wqflask/wqflask/templates/security/_scripts.html | 3 - .../templates/security/change_password.html | 11 ---- .../templates/security/email/change_notice.html | 4 -- .../templates/security/email/change_notice.txt | 5 -- .../security/email/confirmation_instructions.html | 5 -- .../security/email/confirmation_instructions.txt | 5 -- .../security/email/login_instructions.html | 5 -- .../security/email/login_instructions.txt | 5 -- .../security/email/reset_instructions.html | 1 - .../security/email/reset_instructions.txt | 3 - .../templates/security/email/reset_notice.html | 1 - .../templates/security/email/reset_notice.txt | 1 - .../wqflask/templates/security/email/welcome.html | 9 --- .../wqflask/templates/security/email/welcome.txt | 9 --- .../templates/security/forgot_password.html | 9 --- wqflask/wqflask/templates/security/login_user.html | 72 --------------------- .../wqflask/templates/security/register_user.html | 75 ---------------------- .../wqflask/templates/security/reset_password.html | 15 ----- .../templates/security/send_confirmation.html | 34 ---------- wqflask/wqflask/templates/security/send_login.html | 9 --- wqflask/wqflask/templates/security/thank_you.html | 8 --- wqflask/wqflask/user_manager.py | 37 ++++------- 29 files changed, 23 insertions(+), 440 deletions(-) delete mode 100644 wqflask/wqflask/templates/new_security/verified.html delete mode 100644 wqflask/wqflask/templates/security/_macros.html delete mode 100644 wqflask/wqflask/templates/security/_menu.html delete mode 100644 wqflask/wqflask/templates/security/_messages.html delete mode 100644 wqflask/wqflask/templates/security/_scripts.html delete mode 100644 wqflask/wqflask/templates/security/change_password.html delete mode 100644 wqflask/wqflask/templates/security/email/change_notice.html delete mode 100644 wqflask/wqflask/templates/security/email/change_notice.txt delete mode 100644 wqflask/wqflask/templates/security/email/confirmation_instructions.html delete mode 100644 wqflask/wqflask/templates/security/email/confirmation_instructions.txt delete mode 100644 wqflask/wqflask/templates/security/email/login_instructions.html delete mode 100644 wqflask/wqflask/templates/security/email/login_instructions.txt delete mode 100644 wqflask/wqflask/templates/security/email/reset_instructions.html delete mode 100644 wqflask/wqflask/templates/security/email/reset_instructions.txt delete mode 100644 wqflask/wqflask/templates/security/email/reset_notice.html delete mode 100644 wqflask/wqflask/templates/security/email/reset_notice.txt delete mode 100644 wqflask/wqflask/templates/security/email/welcome.html delete mode 100644 wqflask/wqflask/templates/security/email/welcome.txt delete mode 100644 wqflask/wqflask/templates/security/forgot_password.html delete mode 100644 wqflask/wqflask/templates/security/login_user.html delete mode 100644 wqflask/wqflask/templates/security/register_user.html delete mode 100644 wqflask/wqflask/templates/security/reset_password.html delete mode 100644 wqflask/wqflask/templates/security/send_confirmation.html delete mode 100644 wqflask/wqflask/templates/security/send_login.html delete mode 100644 wqflask/wqflask/templates/security/thank_you.html diff --git a/wqflask/wqflask/templates/new_security/forgot_password_step2.html b/wqflask/wqflask/templates/new_security/forgot_password_step2.html index 1295e589..888dcad4 100644 --- a/wqflask/wqflask/templates/new_security/forgot_password_step2.html +++ b/wqflask/wqflask/templates/new_security/forgot_password_step2.html @@ -1,14 +1,9 @@ {% extends "base.html" %} {% block title %}Register{% endblock %} {% block content %} -
-
-

Password Reset

-

- Check your email. -

-
-
+ + {{ header("Password Reset", "Check your email.") }} +
+ + diff --git a/wqflask/wqflask/templates/new_security/forgot_password_step2.html b/wqflask/wqflask/templates/new_security/forgot_password_step2.html index 888dcad4..e32ef753 100644 --- a/wqflask/wqflask/templates/new_security/forgot_password_step2.html +++ b/wqflask/wqflask/templates/new_security/forgot_password_step2.html @@ -10,7 +10,7 @@

One last step

-

You will receive an email with the subject "{{ subject }}".

+

You will receive an email with the subject {{ subject }}.

You must click the link in the email to reset the password.

diff --git a/wqflask/wqflask/templates/new_security/registered.html b/wqflask/wqflask/templates/new_security/registered.html index 5c2b2347..349cbdd4 100644 --- a/wqflask/wqflask/templates/new_security/registered.html +++ b/wqflask/wqflask/templates/new_security/registered.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block title %}Register{% endblock %} {% block content %} - {{ header("Almost Done", "Thanks for registering")} + {{ header("Almost Done", "Thanks for registering") }}
{% endblock %} diff --git a/wqflask/wqflask/templates/new_security/verification_still_needed.html b/wqflask/wqflask/templates/new_security/verification_still_needed.html new file mode 100644 index 00000000..817fa963 --- /dev/null +++ b/wqflask/wqflask/templates/new_security/verification_still_needed.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% block title %}Verification{% endblock %} +{% block content %} + {{ header("Verification", "You still need to verify") }} + +
+ +

You still need to verify your e-mail address before you can sign in.

+ +

We've resent the verificaiton email. + +

Please check for an email with the subject {{ subject }}.

+ +

You must click the link in the email to complete registration.

+ +

If you don't see the email, check your spam folder.

+
+ +{% endblock %} + +{% block js %} + + + {% include "new_security/_scripts.html" %} + + +{% endblock %} diff --git a/wqflask/wqflask/templates/search_result_page.html b/wqflask/wqflask/templates/search_result_page.html index 1fe7cce9..b29e6482 100644 --- a/wqflask/wqflask/templates/search_result_page.html +++ b/wqflask/wqflask/templates/search_result_page.html @@ -27,7 +27,7 @@ Check records below and click Add button to add to selection.

- +
{% for header in header_fields %} @@ -38,10 +38,10 @@ {% for this_trait in trait_list %} - + {% if user.confirmed_at %} - + {% else %} {% endif %} @@ -55,7 +55,7 @@ {% if user.most_recent_login %} - + {% else %} {% endif %} @@ -86,5 +86,5 @@ - + {% endblock %} diff --git a/wqflask/wqflask/templates/collections/list.html b/wqflask/wqflask/templates/collections/list.html new file mode 100644 index 00000000..111d761f --- /dev/null +++ b/wqflask/wqflask/templates/collections/list.html @@ -0,0 +1,50 @@ +{% extends "base.html" %} +{% block title %}Your Collections{% endblock %} +{% block content %} + + {{ header("Your Collections", + 'You have {}.'.format(numify(user_collections|count, "collection", "collections"))) }} + +
+ + +
+
- + Select - - - + + + + +
+ {% endblock %} + +{% block js %} + +{% endblock %} diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index 70aa111e..f519aed9 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -246,6 +246,14 @@ def verify_email(): user.confirmed = json.dumps(basic_info(), sort_keys=True) db_session.commit() + # As long as they have access to the email account + # We might as well log them in + + session_id_signed = successful_login(user) + response = make_response(render_template("new_security/thank_you.html")) + response.set_cookie(UserSession.cookie_name, session_id_signed) + return response + @app.route("/n/password_reset") def password_reset(): print("in password_reset request.url is:", request.url) @@ -325,45 +333,51 @@ def login(): valid = pbkdf2.safe_str_cmp(encrypted.password, pwfields.password) print("valid is:", valid) - login_rec = model.Login(user) - - if valid and not user.confirmed: - # User needs to confirm before we log them in... - flash("You still need to verify your email address." - "We've resent the verification email. " - "Please check your email and follow the instructions.", "alert-error") - VerificationEmail(user) - return redirect((url_for('login'))) - elif valid: - login_rec.successful = True - login_rec.session_id = str(uuid.uuid4()) - #session_id = "session_id:{}".format(login_rec.session_id) - session_id_signature = actual_hmac_creation(login_rec.session_id) - session_id_signed = login_rec.session_id + ":" + session_id_signature - print("session_id_signed:", session_id_signed) - - session = dict(login_time = time.time(), - user_id = user.id, - user_email_address = user.email_address) - - flash("Thank you for logging in {}.".format(user.full_name), "alert-success") + return render_template("new_security/verification_still_needed.html", + subject=VerificationEmail.subject) - key = "session_id:" + login_rec.session_id - print("Key when signing:", key) - Redis.hmset(key, session) + if valid: + session_id_signed = successful_login(user) + flash("Thank you for logging in {}.".format(user.full_name), "alert-success") response = make_response(redirect(url_for('index_page'))) response.set_cookie(UserSession.cookie_name, session_id_signed) else: - login_rec.successful = False + unsuccessful_login(user) flash("Invalid email-address or password. Please try again.", "alert-error") response = make_response(redirect(url_for('login'))) - db_session.add(login_rec) - db_session.commit() + return response + +def successful_login(user): + login_rec = model.Login(user) + login_rec.successful = True + login_rec.session_id = str(uuid.uuid4()) + #session_id = "session_id:{}".format(login_rec.session_id) + session_id_signature = actual_hmac_creation(login_rec.session_id) + session_id_signed = login_rec.session_id + ":" + session_id_signature + print("session_id_signed:", session_id_signed) + + session = dict(login_time = time.time(), + user_id = user.id, + user_email_address = user.email_address) + + key = "session_id:" + login_rec.session_id + print("Key when signing:", key) + Redis.hmset(key, session) + db_session.add(login_rec) + db_session.commit() + return session_id_signed + +def unsuccessful_login(user): + login_rec = model.Login(user) + login_rec.successful = False + db_session.add(login_rec) + db_session.commit() + @app.route("/n/logout") def logout(): print("Logging out...") @@ -459,6 +473,11 @@ def url_for_hmac(endpoint, **values): combiner = "?" return url + combiner + "hm=" + hm +def data_hmac(stringy): + """Takes arbitray data string and appends :hmac so we know data hasn't been tampered with""" + return stringy + ":" + actual_hmac_creation(stringy) + + def verify_url_hmac(url): """Pass in a url that was created with url_hmac and this assures it hasn't been tampered with""" print("url passed in to verify is:", url) @@ -488,7 +507,8 @@ def actual_hmac_creation(stringy): hm = hm[:20] return hm -app.jinja_env.globals.update(url_for_hmac=url_for_hmac) +app.jinja_env.globals.update(url_for_hmac=url_for_hmac, + data_hmac=data_hmac) ####################################################################################### diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py index a060ba3c..6c9addbc 100644 --- a/wqflask/wqflask/views.py +++ b/wqflask/wqflask/views.py @@ -43,6 +43,7 @@ from utility.benchmark import Bench from pprint import pformat as pf from wqflask import user_manager +from wqflask import collect #import logging #logging.basicConfig(filename="/tmp/gn_log", level=logging.INFO) -- cgit v1.2.3 From 5c5671f3c3353f6adc1ce5d9cab8f9f4de5a4e05 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 23 Oct 2013 16:00:40 -0500 Subject: Adding new collections works --- wqflask/wqflask/collect.py | 14 ++++++++-- wqflask/wqflask/model.py | 65 +++++++++------------------------------------- 2 files changed, 24 insertions(+), 55 deletions(-) diff --git a/wqflask/wqflask/collect.py b/wqflask/wqflask/collect.py index 4ec37014..f61ea3dc 100644 --- a/wqflask/wqflask/collect.py +++ b/wqflask/wqflask/collect.py @@ -51,7 +51,10 @@ def collections_add(): @app.route("/collections/new") def collections_new(): - new_collection = request.args['new_collection'] + uc = model.UserCollection() + uc.name = request.args['new_collection'] + print("user_session:", g.user_session.__dict__) + uc.user = g.user_session.record['user_id'] unprocessed_traits = request.args['traits'] print("unprocessed_traits are:", unprocessed_traits) unprocessed_traits = unprocessed_traits.split(",") @@ -64,5 +67,12 @@ def collections_new(): assert hmac==user_manager.actual_hmac_creation(data), "Data tampering?" traits.add(str(data)) + uc.members = json.dumps(list(traits)) print("traits are:", traits) - return "Created: " + new_collection + + db_session.add(uc) + db_session.commit() + + + + return "Created: " + uc.name diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index ca1364d7..c89dc80a 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -7,10 +7,6 @@ import simplejson as json from flask import request from flask.ext.sqlalchemy import SQLAlchemy -#from flask.ext.security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin - -#from flask_security.forms import TextField -#from flask_security.forms import RegisterForm from wqflask import app @@ -19,33 +15,7 @@ from sqlalchemy.orm import relationship, backref from wqflask.database import Base, init_db -# Create database connection object -#db = SQLAlchemy(app) - - -# Is this right? -Sam -#from sqlalchemy.ext.declarative import declarative_base -#Base = declarative_base() - -#@classmethod -#def get(cls, key): -# """Convenience get method using the primary key -# -# If record doesn't exist, returns None -# -# Allows the following: User.get('121') -# -# """ -# print("in get cls is:", cls) -# print(" key is {} : {}".format(type(key), key)) -# query = Model.query(cls) -# print("query is: ", query) -# record = query.get(key) -# return record -# -# -#print("Model is:", vars(Model)) -#Model.get = get + # Define models #roles_users = Table('roles_users', @@ -83,10 +53,7 @@ class User(Base): @property def login_count(self): return self.logins.filter_by(successful=True).count() - #return self.query.filter - #return len(self.logins) - #return 8 - #return len(self.logins.query.filter(User.logins.has(successful=True))) + @property def confirmed_at(self): @@ -104,16 +71,10 @@ class User(Base): return None - - #last_login_at = Column(DateTime()) - #current_login_at = Column(DateTime()) - #last_login_ip = Column(Unicode(39)) - #current_login_ip = Column(Unicode(39)) - #login_count = Column(Integer()) - #roles = relationship('Role', secondary=roles_users, # backref=backref('users', lazy='dynamic')) + class Login(Base): __tablename__ = "login" id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) @@ -127,15 +88,13 @@ class Login(Base): self.user = user.id self.ip_address = request.remote_addr -# Setup Flask-Security -#user_datastore = SQLAlchemyUserDatastore(db, User, Role) - -#class ExtendedRegisterForm(RegisterForm): -# name = TextField('name') -# #print("name is:", name['_name'], vars(name)) -# organization = TextField('organization') -# -#security = Security(app, user_datastore, register_form=ExtendedRegisterForm) - +################################################################################################## -#user_datastore.create_role(name="Genentech", description="Genentech Beta Project(testing)") +class UserCollection(Base): + __tablename__ = "user_collection" + id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) + user = Column(Unicode(36), ForeignKey('user.id')) + name = Column(Text) + created_timestamp = Column(DateTime(), default=lambda: datetime.datetime.utcnow()) + changed_timestamp = Column(DateTime(), default=lambda: datetime.datetime.utcnow()) + members = Column(Text) # We're going to store them as a json list -- cgit v1.2.3 From 78ff756b0d4416f8b1fc4c0008d39bd14b5194f3 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 30 Oct 2013 14:52:09 -0500 Subject: Progress on user collections --- wqflask/wqflask/collect.py | 57 ++++++++++++++++++++++---- wqflask/wqflask/model.py | 7 ++++ wqflask/wqflask/templates/collections/add.html | 55 ++++++++++++++++++++----- wqflask/wqflask/user_manager.py | 17 ++++++++ 4 files changed, 117 insertions(+), 19 deletions(-) diff --git a/wqflask/wqflask/collect.py b/wqflask/wqflask/collect.py index f61ea3dc..39a63a1f 100644 --- a/wqflask/wqflask/collect.py +++ b/wqflask/wqflask/collect.py @@ -46,16 +46,48 @@ from wqflask import user_manager @app.route("/collections/add") def collections_add(): - return render_template("collections/add.html", traits=request.args['traits']) + user_collections = g.user_session.user_ob.user_collections + print("user_collections are:", user_collections) + return render_template("collections/add.html", + traits=request.args['traits'], + user_collections = user_collections, + ) @app.route("/collections/new") def collections_new(): - uc = model.UserCollection() - uc.name = request.args['new_collection'] - print("user_session:", g.user_session.__dict__) - uc.user = g.user_session.record['user_id'] - unprocessed_traits = request.args['traits'] + print("request.args in collections_new are:", request.args) + if "create_new" in request.args: + return create_new() + elif "add_to_existing" in request.args: + return add_to_existing() + elif "continue" in request.args: + return unnamed() + else: + CauseAnError + + +def unnamed(): + return "unnamed" + +def add_to_existing(): + params = request.args + print("---> params are:", params.keys()) + print(" type(params):", type(params)) + uc = model.UserCollection.query.get(params['existing_collection']) + members = set(json.loads(uc.members)) + + traits = process_traits(params['traits']) + + uc.members = json.dumps(list(members | traits)) + + uc.changed_timestamp = datetime.datetime.utcnow() + + db_session.commit() + + return "added to existing, now set is:" + str(uc.members) + +def process_traits(unprocessed_traits): print("unprocessed_traits are:", unprocessed_traits) unprocessed_traits = unprocessed_traits.split(",") traits = set() @@ -66,6 +98,17 @@ def collections_new(): print("hmac is:", hmac) assert hmac==user_manager.actual_hmac_creation(data), "Data tampering?" traits.add(str(data)) + return traits + +def create_new(): + params = request.args + uc = model.UserCollection() + uc.name = params['new_collection'] + print("user_session:", g.user_session.__dict__) + uc.user = g.user_session.user_id + unprocessed_traits = params['traits'] + + traits = process_traits(unprocessed_traits) uc.members = json.dumps(list(traits)) print("traits are:", traits) @@ -73,6 +116,4 @@ def collections_new(): db_session.add(uc) db_session.commit() - - return "Created: " + uc.name diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index c89dc80a..c1ad0a78 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -50,6 +50,10 @@ class User(Base): lazy='dynamic' # Necessary for filter in login_count ) + user_collections = relationship("UserCollection", + order_by="asc(UserCollection.name)", + ) + @property def login_count(self): return self.logins.filter_by(successful=True).count() @@ -98,3 +102,6 @@ class UserCollection(Base): created_timestamp = Column(DateTime(), default=lambda: datetime.datetime.utcnow()) changed_timestamp = Column(DateTime(), default=lambda: datetime.datetime.utcnow()) members = Column(Text) # We're going to store them as a json list + + # This index ensures a user doesn't have more than one collection with the same name + __table_args__ = (Index('usercollection_index', "user", "name"), ) diff --git a/wqflask/wqflask/templates/collections/add.html b/wqflask/wqflask/templates/collections/add.html index dfffff3b..8b6a17a3 100644 --- a/wqflask/wqflask/templates/collections/add.html +++ b/wqflask/wqflask/templates/collections/add.html @@ -1,15 +1,48 @@
+ -
- -
- Create a new collection - - - Type the name of the new collection. - -
-
+ + diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index f519aed9..913ff231 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -77,6 +77,23 @@ class UserSession(object): print("record is:", self.record) self.logged_in = True + @property + def user_id(self): + """Shortcut to the user_id""" + return self.record['user_id'] + + @property + def user_ob(self): + """Actual sqlalchemy record""" + # Only look it up once if needed, then store it + try: + # Already did this before + return self.db_object + except AttributeError: + # Doesn't exist so we'll create it + self.db_object = model.User.query.get(self.user_id) + return self.db_object + def delete_session(self): # And more importantly delete the redis record -- cgit v1.2.3 From de7abc1d2ac9fef87796baa7330cbaaaadbbec2a Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 30 Oct 2013 16:46:53 -0500 Subject: In process on view.html for user_collections --- wqflask/wqflask/templates/collections/view.html | 80 +++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 wqflask/wqflask/templates/collections/view.html diff --git a/wqflask/wqflask/templates/collections/view.html b/wqflask/wqflask/templates/collections/view.html new file mode 100644 index 00000000..3fa83d4a --- /dev/null +++ b/wqflask/wqflask/templates/collections/view.html @@ -0,0 +1,80 @@ +{% extends "base.html" %} +{% block title %}View Collection{% endblock %} +{% block content %} + + {{ header(uc.name, + 'This collection has {}.'.format(numify(results|count, "record", "records"))) }} + +
+ + + + + + + + + + + + + + + + + {% for this_trait in trait_obs %} + + + + {% if this_trait.dataset.type == 'ProbeSet' %} + + + + + + + {% elif this_trait.dataset.type == 'Publish' %} + + + + + + {% elif this_trait.dataset.type == 'Geno' %} + + {% endif %} + + {% endfor %} + + +
DatasetTraid IDSymbolDescriptionLocationMeanNMax LRSMax LRS Location
+ + + + {{ this_trait.name }} + + {{ this_trait.symbol }}{{ this_trait.description_display }}{{ this_trait.location_repr }}{{ this_trait.mean }}{{ this_trait.LRS_score_repr }}{{ this_trait.LRS_location_repr }}{{ this_trait.description_display }}{{ this_trait.authors }} + + {{ this_trait.pubmed_text }} + + {{ this_trait.LRS_score_repr }}{{ this_trait.LRS_location_repr }}{{ this_trait.location_repr }}
+ +
+ + + + + + +
+ + + +{% endblock %} + +{% block js %} + +{% endblock %} -- cgit v1.2.3 From 74d803b0de60edf324b1a6ce589d12a055ef312b Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 30 Oct 2013 19:53:09 -0500 Subject: More work on collections --- wqflask/base/trait.py | 37 ++++--- wqflask/wqflask/collect.py | 36 ++++++- wqflask/wqflask/do_search.py | 120 ++++++++++----------- wqflask/wqflask/model.py | 12 ++- .../wqflask/static/packages/bootstrap/css/docs.css | 16 --- .../wqflask/templates/admin/ind_user_manager.html | 6 +- wqflask/wqflask/templates/collections/list.html | 50 +++++++++ wqflask/wqflask/templates/collections/view.html | 105 +++++++++--------- 8 files changed, 227 insertions(+), 155 deletions(-) create mode 100644 wqflask/wqflask/templates/collections/list.html diff --git a/wqflask/base/trait.py b/wqflask/base/trait.py index 6a64eeaf..aea1f9a9 100755 --- a/wqflask/base/trait.py +++ b/wqflask/base/trait.py @@ -33,6 +33,7 @@ class GeneralTrait(object): assert bool(kw.get('dataset')) != bool(kw.get('dataset_name')), "Needs dataset ob. or name"; if kw.get('dataset_name'): self.dataset = create_dataset(kw.get('dataset_name')) + print(" in GeneralTrait created dataset:", self.dataset) else: self.dataset = kw.get('dataset') self.name = kw.get('name') # Trait ID, ProbeSet ID, Published ID, etc. @@ -41,14 +42,14 @@ class GeneralTrait(object): self.haveinfo = kw.get('haveinfo', False) self.sequence = kw.get('sequence') # Blat sequence, available for ProbeSet self.data = kw.get('data', {}) - + # Sets defaultst self.locus = None self.lrs = None self.pvalue = None self.mean = None self.num_overlap = None - + if kw.get('fullname'): name2 = value.split("::") @@ -57,13 +58,17 @@ class GeneralTrait(object): # self.cellid is set to None above elif len(name2) == 3: self.dataset, self.name, self.cellid = name2 - + # Todo: These two lines are necessary most of the time, but perhaps not all of the time # So we could add a simple if statement to short-circuit this if necessary self.retrieve_info(get_qtl_info=get_qtl_info) self.retrieve_sample_data() - + def get_info(self): + """For lots of traits just use get_trait_info in dataset instead...that will be way + more efficient""" + self.dataset.get_trait_info([self], + webqtlDatabaseFunction.retrieve_species(self.dataset.group.name)) def get_name(self): stringy = "" @@ -77,20 +82,20 @@ class GeneralTrait(object): def get_given_name(self): - """ + """ when user enter a trait or GN generate a trait, user want show the name not the name that generated by GN randomly, the two follow function are used to give the real name and the database. displayName() will show the database also, getGivenName() just show the name. For other trait, displayName() as same as getName(), getGivenName() as same as self.name - + Hongqiang 11/29/07 - + """ stringy = self.name if self.dataset and self.name: - desc = self.dataset.get_desc() + desc = self.dataset.get_desc() if desc: #desc = self.handle_pca(desc) stringy = desc @@ -132,7 +137,7 @@ class GeneralTrait(object): """ export data according to samplelist mostly used in calculating correlation - + """ result = [] for sample in samplelist: @@ -153,7 +158,7 @@ class GeneralTrait(object): """ export informative sample mostly used in qtl regression - + """ samples = [] vals = [] @@ -198,9 +203,9 @@ class GeneralTrait(object): def retrieve_sample_data(self, samplelist=None): if samplelist == None: samplelist = [] - + #assert self.dataset - + #if self.cellid: # #Probe Data # query = ''' @@ -223,7 +228,7 @@ class GeneralTrait(object): # Order BY # Strain.Name # ''' % (self.cellid, self.name, self.dataset.name) - # + # #else: results = self.dataset.retrieve_sample_data(self.name) @@ -330,7 +335,7 @@ class GeneralTrait(object): self.confidential = 1 self.homologeneid = None - + #print("self.geneid is:", self.geneid) #print(" type:", type(self.geneid)) #print("self.dataset.group.name is:", self.dataset.group.name) @@ -394,8 +399,8 @@ class GeneralTrait(object): self.locus_mb = result[1] else: self.locus = self.locus_chr = self.locus_mb = self.lrs = self.pvalue = self.mean = "" - - + + if self.dataset.type == 'Publish': trait_qtl = g.db.execute(""" SELECT diff --git a/wqflask/wqflask/collect.py b/wqflask/wqflask/collect.py index 39a63a1f..ef7b37df 100644 --- a/wqflask/wqflask/collect.py +++ b/wqflask/wqflask/collect.py @@ -40,6 +40,7 @@ from utility import Bunch, Struct from wqflask import user_manager +from base import trait @@ -116,4 +117,37 @@ def create_new(): db_session.add(uc) db_session.commit() - return "Created: " + uc.name + print("Created: " + uc.name) + return redirect(url_for('view_collection', uc_id=uc.id)) + +@app.route("/collections/list") +def list_collections(): + user_collections = g.user_session.user_ob.user_collections + return render_template("collections/list.html", + user_collections = user_collections, + ) + + + +@app.route("/collections/view") +def view_collection(): + params = request.args + uc_id = params['uc_id'] + uc = model.UserCollection.query.get(uc_id) + traits = json.loads(uc.members) + + print("in view_collection traits are:", traits) + + trait_obs = [] + + for atrait in traits: + name, dataset_name = atrait.split(':') + + trait_ob = trait.GeneralTrait(name=name, dataset_name=dataset_name) + trait_ob.get_info() + trait_obs.append(trait_ob) + + return render_template("collections/view.html", + trait_obs=trait_obs, + uc = uc, + ) diff --git a/wqflask/wqflask/do_search.py b/wqflask/wqflask/do_search.py index 5eb09aa1..31f96ced 100644 --- a/wqflask/wqflask/do_search.py +++ b/wqflask/wqflask/do_search.py @@ -26,12 +26,12 @@ class DoSearch(object): assert search_operator in (None, "=", "<", ">", "<=", ">="), "Bad search operator" self.search_operator = search_operator self.dataset = dataset - + if self.dataset: print("self.dataset is boo: ", type(self.dataset), pf(self.dataset)) print("self.dataset.group is: ", pf(self.dataset.group)) #Get group information for dataset and the species id - self.species_id = webqtlDatabaseFunction.retrieve_species_id(self.dataset.group.name) + self.species_id = webqtlDatabaseFunction.retrieve_species_id(self.dataset.group.name) def execute(self, query): """Executes query and returns results""" @@ -44,7 +44,7 @@ class DoSearch(object): #def escape(self, stringy): # """Shorter name than self.db_conn.escape_string""" # return escape(str(stringy)) - + def mescape(self, *items): """Multiple escape""" escaped = [escape(item) for item in items] @@ -63,9 +63,9 @@ class DoSearch(object): class QuickMrnaAssaySearch(DoSearch): """A general search for mRNA assays""" - + DoSearch.search_types['quick_mrna_assay'] = "QuickMrnaAssaySearch" - + base_query = """SELECT ProbeSet.Name as ProbeSet_Name, ProbeSet.Symbol as ProbeSet_Symbol, ProbeSet.description as ProbeSet_Description, @@ -73,9 +73,9 @@ class QuickMrnaAssaySearch(DoSearch): ProbeSet.Mb as ProbeSet_Mb, ProbeSet.name_num as ProbeSet_name_num FROM ProbeSet """ - + header_fields = ['', - 'Record ID', + 'Record', 'Symbol', 'Location'] @@ -112,7 +112,7 @@ class MrnaAssaySearch(DoSearch): FROM ProbeSetXRef, ProbeSet """ header_fields = ['', - 'Record ID', + 'Record', 'Symbol', 'Description', 'Location', @@ -122,7 +122,7 @@ class MrnaAssaySearch(DoSearch): def compile_final_query(self, from_clause = '', where_clause = ''): """Generates the final query string""" - + from_clause = self.normalize_spaces(from_clause) query = (self.base_query + @@ -132,7 +132,7 @@ class MrnaAssaySearch(DoSearch): and ProbeSetXRef.ProbeSetFreezeId = %s """ % (escape(from_clause), where_clause, - escape(self.dataset.id))) + escape(self.dataset.id))) #print("query is:", pf(query)) @@ -149,9 +149,9 @@ class MrnaAssaySearch(DoSearch): GenbankId, UniGeneId, Probe_Target_Description) - AGAINST ('%s' IN BOOLEAN MODE)) + AGAINST ('%s' IN BOOLEAN MODE)) and ProbeSet.Id = ProbeSetXRef.ProbeSetId - and ProbeSetXRef.ProbeSetFreezeId = %s + and ProbeSetXRef.ProbeSetFreezeId = %s """ % (escape(self.search_term[0]), escape(str(self.dataset.id))) @@ -159,7 +159,7 @@ class MrnaAssaySearch(DoSearch): return self.execute(query) - + class PhenotypeSearch(DoSearch): """A search within a phenotype dataset""" @@ -181,9 +181,9 @@ class PhenotypeSearch(DoSearch): 'Publication.Title', 'Publication.Authors', 'PublishXRef.Id') - + header_fields = ['', - 'Record ID', + 'Record', 'Description', 'Authors', 'Year', @@ -237,9 +237,9 @@ class PhenotypeSearch(DoSearch): class QuickPhenotypeSearch(PhenotypeSearch): """A search across all phenotype datasets""" - + DoSearch.search_types['quick_phenotype'] = "QuickPhenotypeSearch" - + base_query = """SELECT Species.Name as Species_Name, PublishFreeze.FullName as Dataset_Name, PublishFreeze.Name, @@ -262,8 +262,8 @@ class QuickPhenotypeSearch(PhenotypeSearch): 'Publication.PubMed_ID', 'Publication.Abstract', 'Publication.Title', - 'Publication.Authors') - + 'Publication.Authors') + def compile_final_query(self, where_clause = ''): """Generates the final query string""" @@ -277,7 +277,7 @@ class QuickPhenotypeSearch(PhenotypeSearch): print("query is:", pf(query)) return query - + def run(self): """Generates and runs a search across all phenotype datasets""" @@ -299,10 +299,10 @@ class GenotypeSearch(DoSearch): FROM GenoXRef, GenoFreeze, Geno """ search_fields = ('Name', 'Chr') - + header_fields = ['', - 'Record ID', - 'Location'] + 'Record', + 'Location'] def get_fields_clause(self): """Generate clause for part of the WHERE portion of query""" @@ -310,7 +310,7 @@ class GenotypeSearch(DoSearch): # This adds a clause to the query that matches the search term # against each field in search_fields (above) fields_clause = [] - + if "'" not in self.search_term[0]: self.search_term = "[[:<:]]" + self.search_term[0] + "[[:>:]]" @@ -419,13 +419,13 @@ class LrsSearch(MrnaAssaySearch): """ DoSearch.search_types['LRS'] = 'LrsSearch' - + def run(self): - + self.search_term = [float(value) for value in self.search_term] - + self.from_clause = ", Geno" - + if self.search_operator == "=": assert isinstance(self.search_term, (list, tuple)) self.lrs_min, self.lrs_max = self.search_term[:2] @@ -444,8 +444,8 @@ class LrsSearch(MrnaAssaySearch): self.sub_clause += """ Geno.Mb > %s and Geno.Mb < %s and """ % self.mescape(min(self.mb_low, self.mb_high), - max(self.mb_low, self.mb_high)) - print("self.sub_clause is:", pf(self.sub_clause)) + max(self.mb_low, self.mb_high)) + print("self.sub_clause is:", pf(self.sub_clause)) else: # Deal with >, <, >=, and <= self.sub_clause = """ %sXRef.LRS %s %s and """ % self.mescape(self.dataset.type, @@ -474,20 +474,20 @@ class CisTransLrsSearch(LrsSearch): print("self.search_term is:", self.search_term) self.search_term = [float(value) for value in self.search_term] self.mb_buffer = 5 # default - + self.from_clause = ", Geno " if self.search_operator == "=": if len(self.search_term) == 2: self.lrs_min, self.lrs_max = self.search_term #[int(value) for value in self.search_term] - + elif len(self.search_term) == 3: self.lrs_min, self.lrs_max, self.mb_buffer = self.search_term - + else: SomeError - + self.sub_clause = """ %sXRef.LRS > %s and %sXRef.LRS < %s and """ % ( escape(self.dataset.type), @@ -510,12 +510,12 @@ class CisTransLrsSearch(LrsSearch): %s.Chr = Geno.Chr""" % ( escape(self.dataset.type), the_operator, - escape(self.mb_buffer), + escape(self.mb_buffer), escape(self.dataset.type), escape(self.species_id), escape(self.dataset.type) ) - + print("where_clause is:", pf(self.where_clause)) self.query = self.compile_final_query(self.from_clause, self.where_clause) @@ -560,7 +560,7 @@ class TransLrsSearch(CisTransLrsSearch): (where the area is determined by the mb_buffer that the user can choose). Opposite of cis-eQTL. """ - + DoSearch.search_types['TRANSLRS'] = "TransLrsSearch" def run(self): @@ -573,7 +573,7 @@ class MeanSearch(MrnaAssaySearch): DoSearch.search_types['MEAN'] = "MeanSearch" def run(self): - + self.search_term = [float(value) for value in self.search_term] if self.search_operator == "=": @@ -599,11 +599,11 @@ class MeanSearch(MrnaAssaySearch): class RangeSearch(MrnaAssaySearch): """Searches for genes with a range of expression varying between two values""" - + DoSearch.search_types['RANGE'] = "RangeSearch" - + def run(self): - + self.search_term = [float(value) for value in self.search_term] if self.search_operator == "=": @@ -632,10 +632,10 @@ class RangeSearch(MrnaAssaySearch): class PositionSearch(DoSearch): """Searches for genes/markers located within a specified range on a specified chromosome""" - + for search_key in ('POSITION', 'POS', 'MB'): - DoSearch.search_types[search_key] = "PositionSearch" - + DoSearch.search_types[search_key] = "PositionSearch" + def setup(self): self.search_term = [float(value) for value in self.search_term] self.chr, self.mb_min, self.mb_max = self.search_term[:3] @@ -646,24 +646,24 @@ class PositionSearch(DoSearch): self.dataset.type, min(self.mb_min, self.mb_max), self.dataset.type, - max(self.mb_min, self.mb_max)) - + max(self.mb_min, self.mb_max)) + def real_run(self): self.query = self.compile_final_query(where_clause = self.where_clause) - return self.execute(self.query) + return self.execute(self.query) class MrnaPositionSearch(MrnaAssaySearch, PositionSearch): """Searches for genes located within a specified range on a specified chromosome""" - + def run(self): self.setup() self.query = self.compile_final_query(where_clause = self.where_clause) return self.execute(self.query) - + class GenotypePositionSearch(GenotypeSearch, PositionSearch): """Searches for genes located within a specified range on a specified chromosome""" @@ -673,12 +673,12 @@ class GenotypePositionSearch(GenotypeSearch, PositionSearch): self.query = self.compile_final_query(where_clause = self.where_clause) return self.execute(self.query) - + class PvalueSearch(MrnaAssaySearch): """Searches for traits with a permutationed p-value between low and high""" - + def run(self): - + self.search_term = [float(value) for value in self.search_term] if self.search_operator == "=": @@ -703,19 +703,19 @@ class PvalueSearch(MrnaAssaySearch): self.query = self.compile_final_query(where_clause = self.where_clause) return self.execute(self.query) - + class AuthorSearch(PhenotypeSearch): """Searches for phenotype traits with specified author(s)""" - - DoSearch.search_types["NAME"] = "AuthorSearch" - + + DoSearch.search_types["NAME"] = "AuthorSearch" + def run(self): self.where_clause = """ Publication.Authors REGEXP "[[:<:]]%s[[:>:]]" and """ % (self.search_term[0]) - + self.query = self.compile_final_query(where_clause = self.where_clause) - + return self.execute(self.query) @@ -741,7 +741,7 @@ if __name__ == "__main__": dataset_name = "HC_M2_0606_P" dataset = create_dataset(db_conn, dataset_name) - + #cursor.execute(""" # SELECT ProbeSet.Name as TNAME, 0 as thistable, # ProbeSetXRef.Mean as TMEAN, ProbeSetXRef.LRS as TLRS, @@ -769,4 +769,4 @@ if __name__ == "__main__": #results = GenotypeSearch("rs13475699", dataset, cursor, db_conn).run() #results = GoSearch("0045202", dataset, cursor, db_conn).run() - print("results are:", pf(results)) \ No newline at end of file + print("results are:", pf(results)) diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index c1ad0a78..cf9e58e1 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -10,7 +10,8 @@ from flask.ext.sqlalchemy import SQLAlchemy from wqflask import app -from sqlalchemy import Column, Integer, String, Table, ForeignKey, Unicode, Boolean, DateTime, Text +from sqlalchemy import (Column, Integer, String, Table, ForeignKey, Unicode, Boolean, DateTime, + Text, Index) from sqlalchemy.orm import relationship, backref from wqflask.database import Base, init_db @@ -98,10 +99,17 @@ class UserCollection(Base): __tablename__ = "user_collection" id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) user = Column(Unicode(36), ForeignKey('user.id')) - name = Column(Text) + + # I'd prefer this to not have a length, but for the index below it needs one + name = Column(Unicode(50)) created_timestamp = Column(DateTime(), default=lambda: datetime.datetime.utcnow()) changed_timestamp = Column(DateTime(), default=lambda: datetime.datetime.utcnow()) members = Column(Text) # We're going to store them as a json list # This index ensures a user doesn't have more than one collection with the same name __table_args__ = (Index('usercollection_index', "user", "name"), ) + + @property + def num_members(self): + print("members are:", json.loads(self.members)) + return len(json.loads(self.members)) diff --git a/wqflask/wqflask/static/packages/bootstrap/css/docs.css b/wqflask/wqflask/static/packages/bootstrap/css/docs.css index 7efd72cd..967989a7 100644 --- a/wqflask/wqflask/static/packages/bootstrap/css/docs.css +++ b/wqflask/wqflask/static/packages/bootstrap/css/docs.css @@ -558,22 +558,6 @@ h2 + .row { border-radius: 4px; } -/* Echo out a label for the example */ -.bs-docs-example:after { - content: "Results"; - position: absolute; - top: -1px; - left: -1px; - padding: 3px 7px; - font-size: 12px; - font-weight: bold; - background-color: #f5f5f5; - border: 1px solid #ddd; - color: #9da0a4; - -webkit-border-radius: 4px 0 4px 0; - -moz-border-radius: 4px 0 4px 0; - border-radius: 4px 0 4px 0; -} /* Remove spacing between an example and it's code */ .bs-docs-example + .prettyprint { diff --git a/wqflask/wqflask/templates/admin/ind_user_manager.html b/wqflask/wqflask/templates/admin/ind_user_manager.html index f99243ac..03a86e63 100644 --- a/wqflask/wqflask/templates/admin/ind_user_manager.html +++ b/wqflask/wqflask/templates/admin/ind_user_manager.html @@ -46,7 +46,7 @@
Confirmed{{ timeago(user.confirmed_at) }}{{ timeago(user.confirmed_at + "Z") }}Unconfirmed
Most recent login{{ timeago(user.most_recent_login.timestamp) }} from {{ user.most_recent_login.ip_address }}{{ timeago(user.most_recent_login.timestamp.isoformat() + "Z") }} from {{ user.most_recent_login.ip_address }}Never
+ + + + + + + + + + + {% for uc in user_collections %} + + + + + + + {% endfor %} + +
NameCreatedLast Changed# Records
{{ uc.name }}{{ timeago(uc.created_timestamp.isoformat() + "Z") }}{{ timeago(uc.changed_timestamp.isoformat() + "Z") }}{{ uc.num_members }}
+ +
+
+ + + +{% endblock %} + +{% block js %} + + + + +{% endblock %} diff --git a/wqflask/wqflask/templates/collections/view.html b/wqflask/wqflask/templates/collections/view.html index 3fa83d4a..0ab004d9 100644 --- a/wqflask/wqflask/templates/collections/view.html +++ b/wqflask/wqflask/templates/collections/view.html @@ -3,73 +3,64 @@ {% block content %} {{ header(uc.name, - 'This collection has {}.'.format(numify(results|count, "record", "records"))) }} + 'This collection has {}.'.format(numify(trait_obs|count, "record", "records"))) }} -
- - - - - - - - - - - - - - +
+ -
- {% for this_trait in trait_obs %} - - - - {% if this_trait.dataset.type == 'ProbeSet' %} - - - - - - - {% elif this_trait.dataset.type == 'Publish' %} - - +
+
DatasetTraid IDSymbolDescriptionLocationMeanNMax LRSMax LRS Location
- - - - {{ this_trait.name }} - - {{ this_trait.symbol }}{{ this_trait.description_display }}{{ this_trait.location_repr }}{{ this_trait.mean }}{{ this_trait.LRS_score_repr }}{{ this_trait.LRS_location_repr }}{{ this_trait.description_display }}{{ this_trait.authors }}
+ + + + + + + + + + + + + + {% for this_trait in trait_obs %} + + + + + + - {% elif this_trait.dataset.type == 'Geno' %} - - {% endif %} - - {% endfor %} - -
RecordDescriptionLocationMeanMax LRSMax LRS Location
- - {{ this_trait.pubmed_text }} + + + + {{ this_trait.name }} {{ this_trait.description_display }}{{ this_trait.location_repr }}{{ this_trait.mean }} {{ this_trait.LRS_score_repr }} {{ this_trait.LRS_location_repr }}{{ this_trait.location_repr }}
+ + {% endfor %} + + + -
+
- - - - - -
+ + + + + + + -- cgit v1.2.3 From 798012e2d21c48b07710660f1f1596bbaf51d2fe Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 31 Oct 2013 20:03:11 -0500 Subject: Implemented superuser and switching users for debugging purposes --- wqflask/wqflask/model.py | 44 +++++++++++++--- wqflask/wqflask/templates/admin/group_manager.html | 12 ++--- .../wqflask/templates/admin/ind_user_manager.html | 33 +++++++++--- wqflask/wqflask/templates/admin/user_manager.html | 9 ++-- wqflask/wqflask/templates/base.html | 2 +- wqflask/wqflask/user_manager.py | 58 +++++++++++++++++++--- 6 files changed, 125 insertions(+), 33 deletions(-) diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index cf9e58e1..b508f18e 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -23,11 +23,11 @@ from wqflask.database import Base, init_db # Column('user_id', Integer(), ForeignKey('user.the_id')), # Column('role_id', Integer(), ForeignKey('role.the_id'))) -class Role(Base): - __tablename__ = "role" - id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) - name = Column(Unicode(80), unique=True, nullable=False) - description = Column(Unicode(255)) +#class Role(Base): +# __tablename__ = "role" +# id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) +# name = Column(Unicode(80), unique=True, nullable=False) +# description = Column(Unicode(255)) class User(Base): __tablename__ = "user" @@ -46,15 +46,27 @@ class User(Base): confirmed = Column(Text) # json detailing when they confirmed, etc. + superuser = Column(Text) # json detailing when they became a superuser, otherwise empty + # if not superuser + logins = relationship("Login", order_by="desc(Login.timestamp)", - lazy='dynamic' # Necessary for filter in login_count + lazy='dynamic', # Necessary for filter in login_count + foreign_keys="Login.user", ) user_collections = relationship("UserCollection", order_by="asc(UserCollection.name)", ) + @property + def name_and_org(self): + """Nice shortcut for printing out who the user is""" + if self.organization: + return "{} from {}".format(self.full_name, self.organization) + else: + return self.full_name + @property def login_count(self): return self.logins.filter_by(successful=True).count() @@ -68,6 +80,23 @@ class User(Base): else: return None + @property + def superuser_info(self): + if self.superuser: + return json.loads(self.superuser) + else: + return None + + @property + def crowner(self): + """If made superuser, returns object of person who did the crowning""" + if self.superuser: + superuser_info = json.loads(self.superuser) + crowner = User.query.get(superuser_info['crowned_by']) + return crowner + else: + return None + @property def most_recent_login(self): try: @@ -89,6 +118,9 @@ class Login(Base): successful = Column(Boolean(), nullable=False) # False if wrong password was entered session_id = Column(Text) # Set only if successfully logged in, otherwise should be blank + # Set to user who assumes identity if this was a login for debugging purposes by a superuser + assumed_by = Column(Unicode(36), ForeignKey('user.id')) + def __init__(self, user): self.user = user.id self.ip_address = request.remote_addr diff --git a/wqflask/wqflask/templates/admin/group_manager.html b/wqflask/wqflask/templates/admin/group_manager.html index df3eda33..ea9026a6 100644 --- a/wqflask/wqflask/templates/admin/group_manager.html +++ b/wqflask/wqflask/templates/admin/group_manager.html @@ -2,11 +2,7 @@ {% block title %}Group Manager{% endblock %} {% block content %} -
-
-

Group Manager

-
-
+ {{ header("List of groups", "" )}}