about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--wqflask/wqflask/collect.py103
-rw-r--r--wqflask/wqflask/model.py44
-rw-r--r--wqflask/wqflask/static/new/javascript/search_results.coffee61
-rw-r--r--wqflask/wqflask/static/new/javascript/search_results.js76
-rw-r--r--wqflask/wqflask/templates/base.html8
-rw-r--r--wqflask/wqflask/templates/collections/add.html10
-rw-r--r--wqflask/wqflask/templates/collections/add_anonymous.html22
-rw-r--r--wqflask/wqflask/templates/collections/view.html13
-rw-r--r--wqflask/wqflask/user_manager.py74
9 files changed, 351 insertions, 60 deletions
diff --git a/wqflask/wqflask/collect.py b/wqflask/wqflask/collect.py
index 6ac4abcf..b1ea46d8 100644
--- a/wqflask/wqflask/collect.py
+++ b/wqflask/wqflask/collect.py
@@ -48,36 +48,51 @@ from base import trait
 
 @app.route("/collections/add")
 def collections_add():
-    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,
-                           )
+    traits=request.args['traits'],
+
+    if g.user_session.logged_in:
+        user_collections = g.user_session.user_ob.user_collections
+        print("user_collections are:", user_collections)
+        return render_template("collections/add.html",
+                               traits=traits,
+                               user_collections = user_collections,
+                               )
+    else:
+        return render_template("collections/add_anonymous.html",
+                               traits=traits
+                               )
 
 
 @app.route("/collections/new")
 def collections_new():
-    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()
+    params = request.args
+    print("request.args in collections_new are:", params)
+
+    collection_name = params['new_collection']
+
+    if "create_new" in params:
+        return create_new(collection_name)
+    elif "add_to_existing" in params:
+        return add_traits(params, collection_name)
+    elif "Default" in params:
+        return add_traits(params, "Default")
+
     else:
         CauseAnError
 
 
-def unnamed():
-    return "unnamed"
 
-def add_to_existing():
-    params = request.args
+def add_traits(params, collection_name):
     print("---> params are:", params.keys())
     print("     type(params):", type(params))
-    uc = model.UserCollection.query.get(params['existing_collection'])
-    members = set(json.loads(uc.members))
+    if collection_name=="Default":
+        uc = g.user_session.user_ob.get_collection_by_name("Default")
+        # Doesn't exist so we'll create it
+        if not uc:
+            return create_new("Default")
+    else:
+        uc = model.UserCollection.query.get(params['existing_collection'])
+    members =  uc.members_as_set() #set(json.loads(uc.members))
     len_before = len(members)
 
     traits = process_traits(params['traits'])
@@ -103,7 +118,8 @@ def add_to_existing():
 
 def process_traits(unprocessed_traits):
     print("unprocessed_traits are:", unprocessed_traits)
-    unprocessed_traits = unprocessed_traits.split(",")
+    if isinstance(unprocessed_traits, basestring):
+        unprocessed_traits = unprocessed_traits.split(",")
     traits = set()
     for trait in unprocessed_traits:
         data, _separator, hmac = trait.rpartition(':')
@@ -114,10 +130,10 @@ def process_traits(unprocessed_traits):
         traits.add(str(data))
     return traits
 
-def create_new():
+def create_new(collection_name):
     params = request.args
     uc = model.UserCollection()
-    uc.name = params['new_collection']
+    uc.name = collection_name
     print("user_session:", g.user_session.__dict__)
     uc.user = g.user_session.user_id
     unprocessed_traits = params['traits']
@@ -136,13 +152,54 @@ def create_new():
 @app.route("/collections/list")
 def list_collections():
     params = request.args
-    user_collections = g.user_session.user_ob.user_collections
+    user_collections = list(g.user_session.user_ob.user_collections)
+    print("user_collections are:", user_collections)
     return render_template("collections/list.html",
                            params = params,
                            user_collections = user_collections,
                            )
 
 
+@app.route("/collections/remove", methods=('POST',))
+def remove_traits():
+    params = request.form
+    print("params are:", params)
+    uc_id = params['uc_id']
+    uc = model.UserCollection.query.get(uc_id)
+    traits_to_remove = params.getlist('traits[]')
+    print("traits_to_remove are:", traits_to_remove)
+    traits_to_remove = process_traits(traits_to_remove)
+    print("\n\n  after processing, traits_to_remove:", traits_to_remove)
+    all_traits = uc.members_as_set()
+    print("  all_traits:", all_traits)
+    members_now = all_traits - traits_to_remove
+    print("  members_now:", members_now)
+    print("Went from {} to {} members in set.".format(len(all_traits), len(members_now)))
+    uc.members = json.dumps(list(members_now))
+    uc.changed_timestamp = datetime.datetime.utcnow()
+    db_session.commit()
+
+    # We need to return something so we'll return this...maybe in the future
+    # we can use it to check the results
+    return str(len(members_now))
+
+
+
+@app.route("/collections/delete", methods=('POST',))
+def delete_collection():
+    params = request.form
+    uc_id = params['uc_id']
+    uc = model.UserCollection.query.get(uc_id)
+    # Todo: For now having the id is good enough since it's so unique
+    # But might want to check ownership in the future
+    collection_name = uc.name
+    db_session.delete(uc)
+    db_session.commit()
+    flash("We've deletet the collection: {}.".format(collection_name), "alert-info")
+
+    return redirect(url_for('list_collections'))
+
+
 @app.route("/collections/view")
 def view_collection():
     params = request.args
diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py
index b508f18e..71bda550 100644
--- a/wqflask/wqflask/model.py
+++ b/wqflask/wqflask/model.py
@@ -10,6 +10,8 @@ from flask.ext.sqlalchemy import SQLAlchemy
 
 from wqflask import app
 
+import sqlalchemy
+
 from sqlalchemy import (Column, Integer, String, Table, ForeignKey, Unicode, Boolean, DateTime,
                         Text, Index)
 from sqlalchemy.orm import relationship, backref
@@ -57,8 +59,35 @@ class User(Base):
 
     user_collections = relationship("UserCollection",
                           order_by="asc(UserCollection.name)",
+                          lazy='dynamic',
                           )
 
+    def display_num_collections(self):
+        """
+        Returns the number of collections or a blank string if there are zero.
+
+
+        Because this is so unimportant...we wrap the whole thing in a try/expect...last thing we
+        want is a webpage not to be displayed because of an error here
+
+        Importand TODO: use redis to cache this, don't want to be constantly computing it
+
+        """
+        try:
+            num = len(list(self.user_collections))
+            return display_collapsible(num)
+        except Exception as why:
+            print("Couldn't display_num_collections:", why)
+            return ""
+
+
+    def get_collection_by_name(self, collection_name):
+        try:
+            collect = self.user_collections.filter_by(name=collection_name).one()
+        except  sqlalchemy.orm.exc.NoResultFound:
+            collect = None
+        return collect
+
     @property
     def name_and_org(self):
         """Nice shortcut for printing out who the user is"""
@@ -145,3 +174,18 @@ class UserCollection(Base):
     def num_members(self):
         print("members are:", json.loads(self.members))
         return len(json.loads(self.members))
+
+    #@property
+    #def display_num_members(self):
+    #    return display_collapsible(self.num_members)
+
+
+    def members_as_set(self):
+        return set(json.loads(self.members))
+
+
+def display_collapsible(number):
+    if number:
+        return number
+    else:
+        return ""
diff --git a/wqflask/wqflask/static/new/javascript/search_results.coffee b/wqflask/wqflask/static/new/javascript/search_results.coffee
index 34989a77..84cf7f0a 100644
--- a/wqflask/wqflask/static/new/javascript/search_results.coffee
+++ b/wqflask/wqflask/static/new/javascript/search_results.coffee
@@ -1,4 +1,8 @@
 $ ->
+    # These are also used by collections view
+    # So the name search_results in the filename is misleading
+
+    checked_traits = null
 
     select_all = ->
         console.log("selected_all")
@@ -17,6 +21,59 @@ $ ->
         console.log("checked is:", traits)
         $.colorbox({href:"/collections/add?traits=#{traits}"})
 
+    removed_traits = ->
+        # After we've removed the traits from the database we get rid of them in the table
+        console.log('in removed_traits with checked_traits:', checked_traits)
+        checked_traits.closest("tr").fadeOut()
+
+
+    change_buttons = ->
+        buttons = ["#add", "#remove"]
+        num_checked = $('.trait_checkbox:checked').length
+        console.log("num_checked is:", num_checked)
+        if (num_checked == 0)
+            for button in buttons
+                $(button).prop("disabled", true)
+        else
+            for button in buttons
+                $(button).prop("disabled", false)
+
+
+        if (num_checked > 1)
+            console.log("in loop")
+            for item in buttons
+                console.log("  processing item:", item)
+                text = $(item).html()
+                if text.indexOf("Records") == -1
+                    text = text.replace("Record", "Records")
+                    $(item).html(text)
+        else
+            console.log("in loop")
+            for item in buttons
+                console.log("  processing item:", item)
+                text = $(item).html()
+                text = text.replace("Records", "Record")
+                $(item).html(text)
+
+
+    # remove is only used by collections view
+    remove = ->
+        checked_traits = $("#trait_table input:checked")
+        traits = checked_traits.map(->
+            return $(this).val()).get()
+        console.log("checked length is:", traits.length)
+        console.log("checked is:", traits)
+        uc_id = $("#uc_id").val()
+        console.log("uc.id is:", uc_id)
+        # Todo: Consider adding a failure message
+        $.ajax(
+              type: "POST"
+              url: "/collections/remove"
+              data:
+                uc_id: uc_id
+                traits: traits
+              success: removed_traits
+            )
 
 
 
@@ -24,3 +81,7 @@ $ ->
     $("#deselect_all").click(deselect_all)
     $("#invert").click(invert)
     $("#add").click(add)
+    $("#remove").click(remove)
+
+    $('.trait_checkbox').click(change_buttons)
+    $('.btn').click(change_buttons)
diff --git a/wqflask/wqflask/static/new/javascript/search_results.js b/wqflask/wqflask/static/new/javascript/search_results.js
index 3efaca8b..477c9b94 100644
--- a/wqflask/wqflask/static/new/javascript/search_results.js
+++ b/wqflask/wqflask/static/new/javascript/search_results.js
@@ -2,7 +2,8 @@
 (function() {
 
   $(function() {
-    var add, deselect_all, invert, select_all;
+    var add, change_buttons, checked_traits, deselect_all, invert, remove, removed_traits, select_all;
+    checked_traits = null;
     select_all = function() {
       console.log("selected_all");
       return $(".trait_checkbox").prop('checked', true);
@@ -24,10 +25,81 @@
         href: "/collections/add?traits=" + traits
       });
     };
+    removed_traits = function() {
+      console.log('in removed_traits with checked_traits:', checked_traits);
+      return checked_traits.closest("tr").fadeOut();
+    };
+    change_buttons = function() {
+      var button, buttons, item, num_checked, text, _i, _j, _k, _l, _len, _len1, _len2, _len3, _results, _results1;
+      buttons = ["#add", "#remove"];
+      num_checked = $('.trait_checkbox:checked').length;
+      console.log("num_checked is:", num_checked);
+      if (num_checked === 0) {
+        for (_i = 0, _len = buttons.length; _i < _len; _i++) {
+          button = buttons[_i];
+          $(button).prop("disabled", true);
+        }
+      } else {
+        for (_j = 0, _len1 = buttons.length; _j < _len1; _j++) {
+          button = buttons[_j];
+          $(button).prop("disabled", false);
+        }
+      }
+      if (num_checked > 1) {
+        console.log("in loop");
+        _results = [];
+        for (_k = 0, _len2 = buttons.length; _k < _len2; _k++) {
+          item = buttons[_k];
+          console.log("  processing item:", item);
+          text = $(item).html();
+          if (text.indexOf("Records") === -1) {
+            text = text.replace("Record", "Records");
+            _results.push($(item).html(text));
+          } else {
+            _results.push(void 0);
+          }
+        }
+        return _results;
+      } else {
+        console.log("in loop");
+        _results1 = [];
+        for (_l = 0, _len3 = buttons.length; _l < _len3; _l++) {
+          item = buttons[_l];
+          console.log("  processing item:", item);
+          text = $(item).html();
+          text = text.replace("Records", "Record");
+          _results1.push($(item).html(text));
+        }
+        return _results1;
+      }
+    };
+    remove = function() {
+      var traits, uc_id;
+      checked_traits = $("#trait_table input:checked");
+      traits = checked_traits.map(function() {
+        return $(this).val();
+      }).get();
+      console.log("checked length is:", traits.length);
+      console.log("checked is:", traits);
+      uc_id = $("#uc_id").val();
+      console.log("uc.id is:", uc_id);
+      return $.ajax({
+        type: "POST",
+        url: "/collections/remove",
+        data: {
+          uc_id: uc_id,
+          traits: traits
+        },
+        success: removed_traits
+      });
+    };
     $("#select_all").click(select_all);
     $("#deselect_all").click(deselect_all);
     $("#invert").click(invert);
-    return $("#add").click(add);
+    $("#add").click(add);
+    $("#remove").click(remove);
+    $('.trait_checkbox').click(change_buttons);
+    return $('.btn').click(change_buttons);
   });
 
 }).call(this);
diff --git a/wqflask/wqflask/templates/base.html b/wqflask/wqflask/templates/base.html
index 9b98c955..48078670 100644
--- a/wqflask/wqflask/templates/base.html
+++ b/wqflask/wqflask/templates/base.html
@@ -104,6 +104,14 @@
                             <a href="/links">Links</a>
                         </li>
                         <li class="">
+                            <a href="/collections/list">Collections
+                                {% if g.user_session.user_ob %}
+                                <span class="badge badge-info">{{ g.user_session.user_ob.display_num_collections() }}</span>
+                                {% endif %}
+                            </a>
+                        </li>
+
+                        <li class="">
                             {% if g.user_session.logged_in %}
                             <a id="login_out" title="Signed in as {{ g.user_session.user_ob.name_and_org }}." href="/n/logout">Sign out</a>
                             {% else %}
diff --git a/wqflask/wqflask/templates/collections/add.html b/wqflask/wqflask/templates/collections/add.html
index 8b6a17a3..faee4f78 100644
--- a/wqflask/wqflask/templates/collections/add.html
+++ b/wqflask/wqflask/templates/collections/add.html
@@ -1,16 +1,16 @@
 <div id="myModal">
     <div class="modal-header">
         <h3>Add to collection</h3>
-        <p>You have three choices: Contuine without naming the collection, creating a new named collection,
-        or adding the traits to an existing collection.</p>
+        <p>You have three choices: Use your default collection, create a new named collection,
+        or add the traits to an existing collection.</p>
     </div>
     <div class="modal-body">
         <form action="/collections/new" data-validate="parsley" id="add_form">
             <fieldset>
-                <legend>Continue without naming collection</legend>
-                <span class="help-block">Choose this if you don't plan on using the collection again.</span>
+                <legend>Use your default collection</legend>
+                <span class="help-block">Choose this if you're in a hurry or don't plan on using the collection again.</span>
                 <span class="help-block"><em></em>If you are unsure this is probably the option you want.</em></span>
-                <button type="submit" name="continue" class="btn">Continue</button>
+                <button type="submit" name="Default" class="btn">Continue</button>
             </fieldset>
             <hr />
 
diff --git a/wqflask/wqflask/templates/collections/add_anonymous.html b/wqflask/wqflask/templates/collections/add_anonymous.html
new file mode 100644
index 00000000..9259f667
--- /dev/null
+++ b/wqflask/wqflask/templates/collections/add_anonymous.html
@@ -0,0 +1,22 @@
+<div id="myModal">
+    <div class="modal-header">
+        <h3>Add to collection</h3>
+        <br />
+        <div>
+            <p>We can add this to a temporary collection for you.</p>
+            <p>For the most features you'll want to sign in or create an account.</p>
+        </div>
+    </div>
+    <div class="modal-body">
+        <form action="/collections/new" data-validate="parsley" id="add_form">
+            <input type="hidden" name="traits" value="{{ traits }}" />
+            <button type="submit" name="Default" class="btn btn-large btn-block btn-primary">Continue without signing in</button>
+            <button type="submit" name="create_new" class="btn btn-large btn-block">Sign in or create an account</button>
+
+        </form>
+    </div>
+</div>
+
+<script>
+    $('#add_form').parsley();
+</script>
diff --git a/wqflask/wqflask/templates/collections/view.html b/wqflask/wqflask/templates/collections/view.html
index 0ab004d9..7f588e84 100644
--- a/wqflask/wqflask/templates/collections/view.html
+++ b/wqflask/wqflask/templates/collections/view.html
@@ -8,8 +8,18 @@
     <div class="container">
         <div class="page-header">
             <h1>Your Collection</h1>
+            <h2>{{ uc.name }}</h2>
+
+            <form action="/collections/delete" method="post">
+                <input type="hidden" name="uc_id" id="uc_id" value="{{ uc.id }}" />
+                <input type="submit"
+                       class="btn btn-small btn-danger"
+                       value="Delete this collection" />
+            </form>
         </div>
 
+
+
         <div class="bs-docs-example">
         <table class="table table-hover" id='trait_table'>
             <thead>
@@ -57,7 +67,8 @@
         <button class="btn" id="select_all"><i class="icon-ok"></i> Select All</button>
         <button class="btn" id="deselect_all"><i class="icon-remove"></i> Deselect All</button>
         <button class="btn" id="invert"><i class="icon-resize-vertical"></i> Invert</button>
-        <button class="btn" id="add"><i class="icon-plus-sign"></i> Add</button>
+        <button class="btn" id="add" disabled="disabled"><i class="icon-plus-sign"></i> Add Record to Other Collection</button>
+        <button class="btn" id="remove" disabled="disabled"><i class="icon-minus-sign"></i> Remove Record</button>
         <button class="btn btn-primary pull-right"><i class="icon-download icon-white"></i> Download Table</button>
         </div>
     </div>
diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py
index 1d0d9846..ff4535bb 100644
--- a/wqflask/wqflask/user_manager.py
+++ b/wqflask/wqflask/user_manager.py
@@ -22,6 +22,7 @@ import urlparse
 
 import simplejson as json
 
+import sqlalchemy
 from sqlalchemy import orm
 
 #from redis import StrictRedis
@@ -188,7 +189,15 @@ class RegisterUser(object):
 
         self.new_user = model.User(**self.user.__dict__)
         db_session.add(self.new_user)
-        db_session.commit()
+
+        try:
+            db_session.commit()
+        except sqlalchemy.exc.IntegrityError:
+            # This exception is thrown if the email address is already in the database
+            # To do: Perhaps put a link to sign in using an existing account here
+            self.errors.append("An account with this email address already exists. "
+                               "Click the button above to sign in using an existing account.")
+            return
 
         print("Adding verification email to queue")
         #self.send_email_verification()
@@ -372,37 +381,44 @@ class LoginUser(object):
         if not params:
             return render_template("new_security/login_user.html")
         else:
-            user = model.User.query.filter_by(email_address=params['email_address']).one()
-            submitted_password = params['password']
-            pwfields = Struct(json.loads(user.password))
-            encrypted = Password(submitted_password,
-                                          pwfields.salt,
-                                          pwfields.iterations,
-                                          pwfields.keylength,
-                                          pwfields.hashfunc)
-            print("\n\nComparing:\n{}\n{}\n".format(encrypted.password, pwfields.password))
-            valid = pbkdf2.safe_str_cmp(encrypted.password, pwfields.password)
-            print("valid is:", valid)
-
-            if valid and not user.confirmed:
-                VerificationEmail(user)
-                return render_template("new_security/verification_still_needed.html",
-                                       subject=VerificationEmail.subject)
-
-
-            if valid:
-                if params.get('remember'):
-                    print("I will remember you")
-                    self.remember_me = True
-
-                return self.actual_login(user)
-
+            try:
+                user = model.User.query.filter_by(email_address=params['email_address']).one()
+            except sqlalchemy.orm.exc.NoResultFound:
+                print("No account exists for that email address")
+                valid = False
+                user = None
             else:
+                submitted_password = params['password']
+                pwfields = Struct(json.loads(user.password))
+                encrypted = Password(submitted_password,
+                                              pwfields.salt,
+                                              pwfields.iterations,
+                                              pwfields.keylength,
+                                              pwfields.hashfunc)
+                print("\n\nComparing:\n{}\n{}\n".format(encrypted.password, pwfields.password))
+                valid = pbkdf2.safe_str_cmp(encrypted.password, pwfields.password)
+                print("valid is:", valid)
+
+        if valid and not user.confirmed:
+            VerificationEmail(user)
+            return render_template("new_security/verification_still_needed.html",
+                                   subject=VerificationEmail.subject)
+
+
+        if valid:
+            if params.get('remember'):
+                print("I will remember you")
+                self.remember_me = True
+
+            return self.actual_login(user)
+
+        else:
+            if user:
                 self.unsuccessful_login(user)
-                flash("Invalid email-address or password. Please try again.", "alert-error")
-                response = make_response(redirect(url_for('login')))
+            flash("Invalid email-address or password. Please try again.", "alert-error")
+            response = make_response(redirect(url_for('login')))
 
-                return response
+            return response
 
     def actual_login(self, user, assumed_by=None):
         """The meat of the logging in process"""