Source: trellinator-libs/Card.js

/**
* @class Card
* @memberof module:TrelloEntities
* @param data (Object} key/value pairs of 
* information, must at least contain "id",
* can basically just pass in response from Trello API
* @constructor
* @classdesc The Card class represents
* a Card in Trello. Not every Notification will
* have a card object associated with it because
* all Trellinator webhooks are registered at the
* board level (so for example a notification about
* the name of a list being updated won't have a 
* card object associated with it).
* 
* Cards are loaded from Boards or Lists, and must
* be created in a List.
*
* @example
* new Notification(posted).card().postComment("Hello world!");
* @example
* new Trellinator().board("Some Board").card(new RegExp("Find me.*"));
* @example
* new Notification(posted).board().list("ToDo").cards().first().moveToNextList();
* @example
* Card.create(new Trellinator().board("Some board").list("ToDo"),"Do it!");
* @xample
* Card.create(new Trellinator().board("Some board").list("ToDo"),{name: "Do it!"});
* @xample
* Card.findOrCreate(new Trellinator().board("Some board").list("ToDo"),"Do it!");
* @xample
* Card.findOrCreate(new Trellinator().board("Some board").list("ToDo"),{name: "Do it!"});
*/
var Card = function(data)
{    
    this.data            = data;
    this.notification_object = null;
    this.checklist_list  = null;
    this.labels_list     = null;
    this.members_list    = null;
    this.current_list = null;
  this.containing_board = null;

    /**
    * Returns the card ID
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).card().id();
    */
    this.id = function()
    {
        return this.data.id;
    }
    
    this.setNotification = function(notif)
    {
        this.notification_object = notif;
        return this;
    }
    
    /**
    * Return a Date object representing the
    * creation date of this card
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).card().whenCreated().toLocaleString();
    */    
    this.whenCreated = function()
    {
      return new Date(1000*parseInt(this.id().substring(0,8),16));
    }
    
    /**
    * Number of days excluding saturday and sunday
    * since this card was created
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).card().weekDaysSinceCreated().toLocaleString();    
    */
    this.weekDaysSinceCreated = function()
    {
      var created = this.whenCreated().addDays(1);
      var ret = 0;
      
      while(created < Trellinator.now())
      {
        if(created.isWeekDay())
          ret++;
        
        created.addDays(1);
      }
      
      return ret;
    }
    
    /**
    * Return the notification (if any) that
    * originated this card
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).card().notification().replytoMember("Hai");
    */
    this.notification = function()
    {
        return this.notification_object;
    }

    /**
    * Removes any item from any checklist on 
    * this card matching the string or RegExp
    * passed in
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} the text (exact or regex match) 
    * of the checklist item to remove
    * @example
    * new Notification(posted).card().removeChecklistItemByName(new RegExp("Milk.*"));
    */
    this.removeChecklistItemByName = function(name)
    {
        this.checklists().each(function(list)
        {   
            list.items().each(function(item)
            {   
                if(TrelloApi.nameTest(name,item.name()))
                    item.remove();
            });
        });
        
        this.checklist_list = null;
        return this;
    }

    /**
    * Return true if the due date on this 
    * card has been marked complete
    * @memberof module:TrelloEntities.Card
    * @example
    * var card = new Notification(posted).card();
    *
    * if(card.dueComplete())
    *     card.moveToNextList();
    */
    this.dueComplete = function()
    {
        if(typeof this.data.dueComplete == "undefined")
            this.load();

        return this.data.dueComplete;
    }

    /**
    * Return the Board object that this card
    * is on
    * @memberof module:TrelloEntities.Card
    * @example
    * var card = new Notification(posted).card();
    * card.board().card(card.name()).each(function(loop)
    * {
    *     if(loop.id() != card.id())
    *         loop.postComments("Twinsies with "+card.link());
    * }
    */
    this.board = function()
    {
        var ret = null;

        if(this.containing_board)
            ret = this.containing_board;
      
        else if(this.current_list)
        {
            ret = this.current_list.board();
        }
        
        else
        {
            if(!this.data.idBoard && !this.data.board)
                this.load();
            
            var data = (this.data.board) ? this.data.board:{id: this.data.idBoard};
            ret = new Board(data);
            this.containing_board = ret;
        }
        
        if(!ret)
            throw new InvalidDataException("Board not found for card: "+this.id());
        
        return ret;
    }
    
    this.setContainingBoard = function(board)
    {
      this.containing_board = board;
      return this;
    }

    /**
    * Return a list of comments from this card
    * @memberof module:TrelloEntities.Card
    * @param limit {int} (optional) limit the number of comments
    * returned, default limit is 20
    * @example
    * new Notification(posted).card().comments().each(function(comment)
    * {
    *     Card.create(new Trellinator().board("Some Board").list("ToDo"),comment);
    * });
    */
    this.comments = function(limit)
    {
        if(!limit)
            limit = 20;

        return new IterableCollection(TrelloApi.get("cards/"+this.data.id+"/actions?filter=copyCommentCard,commentCard&limit="+limit))
                                               .transform(function(elem)
        {
            return new Comment(elem);
        });
    }

    /**
    * Return the date/time this card was moved
    * into it's current list
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).card().movedToList().toLocaleString();
    */
    this.movedToList = function()
    {
        var res = TrelloApi.get("cards/"+this.data.id+"/actions?filter=updateCard:idList&limit=1");
        
        if(!res.length)
            res = TrelloApi.get("cards/"+this.data.id+"/actions?filter=createCard&limit=1");
        
        if(res.length)
          var ret = new Date(res[0].date);
        else
          var ret = Trellinator.now();
      
        this.moved_to_list_cache = ret;
        return this.moved_to_list_cache;
    }

    /**
    * Move the card to the next list in the same board
    * or throw InvalidDataException if there is no next
    * list
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).card().moveToNextList();
    */
    this.moveToNextList = function()
    {
        this.moveToList(
          this.currentList()
          .board()
          .lists()
          .itemAfter(this.currentList().id(),
                     function(test,elem)
                     {
                       return test == elem.id();
                     }),"top");
        return this;
    }

    /**
    * Return the List this card is currently in
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).archivedCard().currentList().archive();
    */
    this.currentList = function()
    {
      var ret = null;
      
      if(!this.data.list && !this.current_list)
          this.load();
      
      if(this.current_list)
          ret = this.current_list;
      
      else if(this.data.list)
          ret = new List(this.data.list);
      
      if(!ret)
        throw new InvalidDataException("Card is not in a list: "+this.id());        
      
      return ret;
    }
    
    this.setCurrentList = function(list)
    {
      this.current_list = list;
      return this;
    }
    
    /**
    * Return true if all checklists on a card
    * are complete
    * @memberof module:TrelloEntities.Card
    * @see Notification.completedAllChecklists()
    * @example
    * new Trellinator().board("Some Board").list("Doing").cards().each(function(card)
    * {
    *     if(card.allChecklistsComplete())
    *         card.moveToNextList();
    * });
    * @example
    * //This is not part of this class, but a common use case you 
    * //should be aware of instead
    * new Notification(posted).completedAllChecklists().moveToNextList();
    */
    this.allChecklistsComplete = function()
    {
        var ret = true;

        this.checklists().each(function(checklist)
        {   
            checklist.items().each(function(item)
            {   
              ret = item.isComplete();
            }.bind(this));
        }.bind(this));        
        
        return ret;
    }

    /**
    * Return an IterableCollection of all cards linked as attachments
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).card().cardsLinkedInAttachments().first().postComment("Hello from over here");
    */
    this.cardsLinkedInAttachments = function()
    {
        return this.attachments(TrelloApi.cardLinkRegExp()).find(function(elem)
        {
          try
          {
            return new Card({link: elem.link()});
          }
          
          catch(e)
          {
            if(
              (e.toString().indexOf("card not found") === 0) ||
              (e.toString().indexOf("unauthorized card permission requested") === 0)
            )
            {
              return false;
            }
            else
            {
              throw e;
            }
          }
        });
    }

    /**
    * Return an IterableCollection of all boards linked as attachments
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).card().boardsLinkedInAttachments().first().list("ToDo").cards().each(function(card)
    * {
    *     card.postComment("Ger er done!");
    * });
    */
    this.boardsLinkedInAttachments = function()
    {
        return this.attachments(TrelloApi.boardLinkRegExp()).find(function(elem)
        {
          return new Board({link: elem.link()});
        });
    }

    /**
    * Fetch the first attachment on the card. Name filtering
    * not implemented yet
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} not yet implemented
    * @example
    * Trellinator.log(new Notification(posted).card().attachment());
    */
    this.attachment = function(name)
    {
        return this.attachments(name).first();
    }

    /**
    * Get all attachments on the card, filtering
    * not implemented
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} name or RegExp, not 
    * yet implemented
    * @example
    * new Notification(posted).card().attachments().each(function(att)
    * {
    *     Trellinator.log(att);
    * });
    */
    this.attachments = function(name)
    {
        if(!this.data.attachments)
            this.load();

        return new IterableCollection(this.data.attachments).find(function(elem)
        {
            var totest = new Attachment(elem);
            var ret = false;
          
            if(name && TrelloApi.nameTest(name,totest.text()))
                ret = new Attachment(elem);
            else if(name && TrelloApi.nameTest(name,totest.link()))
                ret = new Attachment(elem);
            else if(!name)
                ret = totest;

            if(ret)
                ret.setContainingCard(this);

            return ret;
        }.bind(this));
    }

    /**
    * Return a link to this card
    * @memberof module:TrelloEntities.Card
    * @example
    * var notif = new Notification(posted);
    * notif.card().cardsLinkedInAttachments().first().checkItemByName(notif.card().link());
    */
    this.link = function()
    {
        return "https://trello.com/c/"+this.shortId();
    }

    /**
    * Return the short ID of this card
    * @memberof module:TrelloEntities.Card
    * @example
    * var notif = new Notification(posted);
    * notif.card().shortId();
    */
    this.shortId = function()
    {
        if(!this.data.shortLink)
            this.load();
        
        return this.data.shortLink;
    }
    
    
    /**
    * Return a link to this card formatted so
    * that it can be used in a checklist item name
    * such that the link will work on both mobile and
    * web/desktop apps
    * @memberof module:TrelloEntities.Card
    * @example
    * var notif = new Notification(posted);
    * notif.card().mobileFriendlyLink();
    */
    this.mobileFriendlyLink = function()
    {
      return "["+this.name().replaceAll("@","AT").replaceAll(/[<>"]/,"")+"]("+this.link()+")";
    }
    
    /**
    * Add a link attachment to this card
    * @memberof module:TrelloEntities.Card
    * @param data {string|Object} either a string that is a fully 
    * formed URL, or an object that contains at least either
    * an attribute link or url, and optionally one of these as
    * well as a name attribute
    * @example
    * var notif = new Notification(posted);
    *
    * var notif.card().addChecklist("Linked Cards",function(list)
    * {
    *     notif.board().list("ToDo").cards().each(function(card)
    *     {
    *         list.addItem(card.link());
    *         card.attachLink(notif.card().link());
    *     });
    * });
    * @example
    * new Notification(posted).card().attachLink({name: "A Popular Search Engine",url: "https://www.google.com/"});
    * @example
    * new Notification(posted).card().attachLink({name: "A Popular Search Engine",link: "https://www.google.com/"});
    */
    this.attachLink = function(data)
    {
      if(data.url)
        var link = data.url;
      else if(typeof data.link == "string")
        var link = data.link;
      else
        var link = data;
      
      var url = "cards/"+this.data.id+"/attachments?url="+encodeURIComponent(link);
      
      if(data.name)
      {
        var maxlength = 256;
        var ltrimmed_name = data.name.substr(data.name.length-maxlength);
        url += "&name="+encodeURIComponent(ltrimmed_name);
      }
      
      TrelloApi.post(url);
      
      if(this.data.attachments)
        this.data.attachments = null;
      
      return this;
    }
    
    /**
    * Download a file from a URL to Google Drive, then add it
    * as a link to the card. I can't see a good way to add a file
    * directly by URL either from the internet or from Google Drive
    * so this should be updated at some point.
    * @memberof module:TrelloEntities.Card
    * @param data {string|Object} either a string that is a fully 
    * formed URL, or an object that contains at least either
    * an attribute link or url, and optionally one of these as
    * well as a name attribute
    * @example
    * var notif = new Notification(posted);
    * card.attachFile(notif.card().attachments().first().link());
    */
    this.attachFile = function(data)
    {
        if(data.url)
            var link = data.url;
        else if(typeof data.link == "string")
            var link = data.link;
        else
            var link = data;
      
      var file = Trellinator.downloadFileToGoogleDrive(link);
      this.attachLink({name: file.getName(),url: file.getUrl()});
    }

    /**
    * Set the name of this card
    * @memberof module:TrelloEntities.Card
    * @param name {string} the new name for the card
    * @example
    * new Notification(posted).card().setName("UPDATED");
    */
    this.setName = function(name)
    {
        TrelloApi.put("cards/"+this.data.id+"?name="+encodeURIComponent(name));
        this.data.name = name;
        this.data.text = name;
        return this;
    }

    /**
    * Return the name of this card (also sometimes
    * called the title of the card)
    * @memberof module:TrelloEntities.Card
    * @example
    * Trellinator.log(new Notification(posted).card().name());
    */
    this.name = function()
    {
        if((typeof this.data.name === 'undefined') && (typeof this.data.text === 'undefined'))
            this.load();

        return this.data.text ? this.data.text:this.data.name;
    }
    
    /**
    * Add a member to this card
    * @memberof module:TrelloEntities.Card
    * @param member {Member} a Member object to add to the
    * card
    * @example
    * var notif = new Notification();
    * var created = notif.createdCard();
    *
    * notif.board().members().each(function(member)
    * {
    *     notif.card().addMember(member);
    * });
    */
    this.addMember = function(member)
    {
        try
        {
            TrelloApi.post("cards/"+this.data.id+"/idMembers?value="+member.data.id);
            this.members_list    = null;
        }
        
        catch(e)
        {
            Notification.expectException(InvalidRequestException,e);
            
            if(e.toString().indexOf("member is already on the card") == -1)
                throw e;
        }

        return this;
    }

    /**
    * Return a Member of this card matching
    * the given name/regex
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} a name or RegExp to match
    * @example
    * new Notification(posted).card().member("iaindooley");
    */
    this.member = function(name)
    {
        return this.members(name).first();
    }
    
    /**
    * Return an IterableCollection of Members on this card
    * optionally filtered by name using a string or RegExp
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} a name or RegExp to filter by
    * @example
    * var card = new Notification(posted).archivedCard();
    *
    * card.members().each(function(member)
    * {
    *     card.postComment("@"+member.name()+" this card was archived");
    * });
    */
    this.members = function(name)
    {
        if(!this.members_list)
        {
            if(!this.data.members)
                this.load();

            this.members_list = new IterableCollection(this.data.members).transform(function(elem)
            {
                return new Member(elem);
            });
        }
                       
        return this.members_list.findByName(name);
    }
    
    /**
    * Return a Card if there is one linked in the
    * description of this card
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).archivedCard().cardLinkedInDescription().postComment("Your pal was archived");
    */
    this.cardLinkedInDescription = function()
    {
        if(parts = TrelloApi.cardLinkRegExp().exec(this.description()))
            var ret = new Card({id: parts[1]});
        else
            throw new InvalidDataException("No card linked in description");
        
        return ret;
    }

    /**
    * Return the description of this card
    * @memberof module:TrelloEntities.Card
    * @example
    * var card = new Notification(posted).changedCardName();
    * 
    * card.setDescription(Trellinator.now().toLocaleString()+" updated to "+card.name()+"\n\n"+card.description());
    */
    this.description = function()
    {
        if(!this.data.desc)
            this.load();
        
        return this.data.desc;
    }

    /**
    * Return a Label if it is on this card, or false
    * if the label is not on the card
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} a string or RegExp to match
    * the label name against
    * @example
    * //check if a due date was marked complete on a card with label starting with "Process"
    * var added = new Notification(posted).addedLabel("Old");
    * 
    * if(added.card().hasLabel("New"))
    *     added.card().postComment("Something old and something new");
    */
    this.hasLabel = function(name)
    {
        try
        {
            return this.labels(name).first();
        }
        
        catch(e)
        {
            return false;
        }
    }

    /**
    * Return a Label if it is on this card, or throw
    * InvalidDataException if it isn't on the card
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} a string or RegExp to match
    * the label name against
    * @example
    * //check if a due date was marked complete on a card with label starting with "Process"
    * new Notification(posted).completedDueDate().label(new RegExp("Process.*"));
    */
    this.label = function(name)
    {
        return this.labels(name).first();
    }

    /**
    * Return an IterableCollection of Label objects that
    * are on this card optionally filtered by name/regexp
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} a string or RegExp to filter
    * against
    * @example
    * new Notification(posted).card().labels().each(function(label)
    * {
    *     Trellinator.log(label.name());
    * });
    */
    this.labels = function(name)
    {
        if(!this.labels_list)
        {
            if(!this.data.labels)
                this.load();
    
            try
            {
              this.labels_list = new IterableCollection(this.data.labels).transform(function(elem)
              {
                  return new Label(elem).setContainingCard(this);
              }.bind(this));
            }
            
            catch(e)
            {
              throw new InvalidDataException("No labels present on card");
            }
        }
        
        return this.labels_list.findByName(name);
    }
    
    /**
    * Set the description of this card
    * @memberof module:TrelloEntities.Card
    * @param desc {string} The description to set on the card
    * @example
    * var card = new Notification(posted).archivedCard();
    * card.setDescription("Archived on: "+Trellinator.now().toLocaleString()+"\n\n"+card.description());
    */
    this.setDescription = function(desc)
    {
        if(desc.length > 16384)
            desc = desc.substring(0,16381)+"...";

        TrelloApi.put("cards/"+this.data.id+"?desc="+encodeURIComponent(desc));
        this.data.desc = desc;
        return this;
    }

    /**
    * Post a comment to this card
    * @memberof module:TrelloEntities.Card
    * @param comment_text {string} the text to post
    * @example
    * var card = new Notification(posted).movedCard("Done");
    * 
    * card.members().each(function(member)
    * {
    *     card.postComment("@"+member.name()+" this card was moved to the Done list");
    * });
    */
    this.postComment = function(comment_text)
    {
        if(comment_text.length > 16384)
            comment_text = comment_text.substring(0,16381)+"...";

        TrelloApi.post("cards/"+this.data.id+"/actions/comments?text="+encodeURIComponent(comment_text));
        return this;
    }

    /**
    * Return the due date for this Card, which 
    * can be passed into the constructor of a Date object
    * @memberof module:TrelloEntities.Card
    * @example
    * new Date(new Notification(posted).card().due());
    */
    this.due = function()
    {
        if(!this.data.due)
            this.load();
        
        return this.data.due;
    }
    
    /**
    * Remove all members from this card
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).movedCard("Done").removeAllMembers();
    */
    this.removeAllMembers = function()
    {
        this.members().each(function(elem)
        {
            this.removeMember(elem);
        }.bind(this));
      
        this.members_list = null;
        return this;
    }

    /**
    * Remove all labels from this card
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).movedCard("Done").removeAllLabels();
    */
    this.removeAllLabels = function()
    {
        this.labels().each(function(elem)
        {
            this.removeLabel(elem);
        }.bind(this));
      
        this.labels_list = null;
        return this;
    }
    
    /**
    * Remove a member from this card
    * @memberof module:TrelloEntities.Card
    * @param member {Member} a Member object to remove from this
    * card
    * @example
    * var notif  = new Notification(posted);
    * 
    * if(new RegExp("Remove.*").test(notif.addedComment().text()))
    * {
    *    notif.card().removeMember(notif.member());
    * }
    */
    this.removeMember = function(member)
    {
        try
        {
            TrelloApi.del("cards/"+this.data.id+"/idMembers/"+member.data.id);
            this.members_list = null;
        }

        catch(e)
        {
            if(e.toString().indexOf("member is not on the card") == -1)
                throw e;
        }

        return this;
    }

    /**
    * Mark the due date on this card complete
    * @memberof module:TrelloEntities.Card
    * @example
    * //Mark the due date complete on a card that was moved into the Done list
    * new Notification(posted).movedCard("Done").markDueDateComplete();
    */
    this.markDueDateComplete = function()
    {
        TrelloApi.put("cards/"+this.data.id+"?dueComplete=true");
        this.data.dueComplete = true;
        return this;
    }

    /**
    * Mark the due date on this card incomplete
    * @memberof module:TrelloEntities.Card
    * @example
    * //Mark the due date complete on a card that was moved into the Done list
    * new Notification(posted).movedCard("Done").markDueDateIncomplete();
    */
    this.markDueDateIncomplete = function()
    {
        TrelloApi.put("cards/"+this.data.id+"?dueComplete=false");
        this.data.dueComplete = false;
        return this;
    }

    /**
    * Clear the due date on this card
    * @memberof module:TrelloEntities.Card
    * @example
    * //Remove the due date on a card that was moved to the Backlog list
    * new Notification(posted).movedCard("Backlog").removeDueDate();
    */
    this.removeDueDate = function()
    {
        TrelloApi.put("cards/"+this.data.id+"?due=null");
        this.data.due = null;
        return this;
    }

    /**
    * Set the due date on this card
    * @memberof module:TrelloEntities.Card
    * @param datetime {Date} a Date object
    * @example
    * //When a card is created in the ToDo list set it due in 3 days time at 9am 
    * new Notification(posted).createdCard("ToDo").setDue(Trellinator.now().addDays(3).at("9:00"));
    */
    this.setDue = function(datetime)
    {
        TrelloApi.put("cards/"+this.data.id+"?due="+encodeURIComponent(datetime.toUTCString()));
        this.data.due = datetime;
        return this;
    }

    /**
    * Copy this card to a given List, optionally to 
    * a specific position, and return the copy
    * @memberof module:TrelloEntities.Card
    * @param list {List} a List object to copy this card to
    * @param position {string|int} either top, bottom or a number
    * @example
    * var list = new Trellinator().board("Some Board").findOrCreateList("ToDo");
    * //Copy the card to a list ToDo in the board Some Board in position 2
    * new Notification(posted).card().copyToList(list,2);
    */
    this.copyToList = function(list,position,keep)
    {
        if(!position)
            position = "top";
        if(!keep)
            keep = "all";

        list.card_list = null;
        return new Card(TrelloApi.post("cards?pos="+position+"&idList="+list.data.id+"&idCardSource="+this.data.id+"&keepFromSource="+encodeURIComponent(keep)));
    }

    /**
    * Copy this card to a list on the same board
    * and return the copy
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} a string list name, or 
    * regex (if multiple matches, the first matching list will be used
    * @param position {string|int} top, bottom or a number
    * @example
    * new Notification(posted).card().copyTo("ToDo","top");
    */
    this.copyTo = function(name,position,keep)
    {
        if(!position)
            position = (name.position)?name.position:"top";

        return this.copyToList(this.board().list(TrelloApi.nameTestData(name,"list")),position,keep);
    }

    /**
    * Move a card to a given List
    * @memberof module:TrelloEntities.Card
    * @param list {List} a list object to move the card to
    * @param position {string|int} top, bottom or a number (defaults to bottom)
    * @example
    * var to_list = new Trellinator().board("Other Board").list("ToDo");
    * new Notification(posted).createdCard("Doing").moveToList(to_list,"top");
    */
    this.moveToList = function(list,position)
    {
        if(!position)
            position = "bottom";

        TrelloApi.put("cards/"+this.data.id+"?idList="+list.data.id+"&idBoard="+list.board().data.id+"&pos="+position);
        this.data.list = null;
        
        if(this.current_list)
        {
            this.current_list.card_list = null;
            this.current_list = null;
        }

        this.current_list = null;
        this.moved_to_list_cache = null;
        list.card_list = null;
        this.containing_board = null;
        return this;
    }

    /**
    * Move a card to a list within the same board
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} the name or a regex to match a 
    * list within the same board to move the card to
    * @param position {string|int} (optional) top, bottom or a number, default bottom
    * @example
    * new Notification(posted).archivedCard().unArchive().moveTo("Graveyard","top");
    */
    this.moveTo = function(name,position)
    {
        if(!position)
            position = (name.position)?name.position:"bottom";

        return this.moveToList(this.board().list(TrelloApi.nameTestData(name,"list")),position);
    }
    
    /**
    * Delete this card NO UNDO AVAILABLE
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).movedCard("Done").del();
    */
    this.del = function()
    {
        TrelloApi.del("cards/"+this.data.id);
        return this;
    }

    /**
    * Archive this card
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).movedCard("Done").archive();
    */
    this.archive = function()
    {
        TrelloApi.put("cards/"+this.data.id+"?closed=true");
        this.currentList().card_list = null;
        return this;
    }

    /**
    * Unarchive this card
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).archivedCard().unArchive();
    */
    this.unArchive = function()
    {
        TrelloApi.put("cards/"+this.data.id+"?closed=false");
        this.currentList().card_list = null;
        return this;
    }
    
    /**
    * Check if this card is archived
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).archivedCard().isArchived();
    */
    this.isArchived = function()
    {
      if(typeof this.data.closed === 'undefined')
      {
        this.load();
      }
      
      return this.data.closed;
    }
    
    /**
    * Return a checklist from this card of the given
    * name if it exists or throw InvalidDataException
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} a name or RegExp to match against
    * the checklist name (first will be returned if multiple match)
    * @example
    * new Notification(posted).movedCard("Done").checklist(new RegExp("Process.*")).markAllItemsComplete();
    */
    this.checklist = function(name)
    {
        return this.checklists(name).first();
    }

    /**
    * Return an IterableCollection of Checklist objects
    * optionally filtered by name/RegExp
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} a string or regex to filter the list by
    * @example
    * new Notification(posted).completedDueDate().checklists(new RegExp("Process.*")).each(function(list)
    * {
    *     list.markAllItemsComplete();
    * });
    */
    this.checklists = function(name)
    {
        if(!this.checklist_list)         
        {
            this.checklist_list = new IterableCollection(TrelloApi.get("cards/"+this.data.id+"/checklists")).transform(function(elem)
            {
              return new Checklist(elem).setContainingCard(this);
            }.bind(this))
        }

        return this.checklist_list.findByName(name);
    }

    /**
    * Check any item in any checklist with the given
    * name or matching RegExp
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} the name of the checklist item
    * to complete, or a RegExp (all matching items will be completed)
    * @example
    * var card = new Notification(posted).archivedCard();
    * cards.cardsLinkedInAttachments().first().checkItemByName(card.link());
    */
    this.checkItemByName = function(name)
    {
        this.checklists().each(function(checklist)
        {
            checklist.items().each(function(item)
            {
                if((item.state() == "incomplete") && TrelloApi.nameTest(name,item.name()))
                    TrelloApi.put("cards/"+this.data.id+"/checkItem/"+item.data.id+"?state=complete");
            }.bind(this));
        }.bind(this));
        
        this.checklist_list  = null;
        return this;
    }

    /**
    * Copy a checklist from this card, to another
    * card if it doesn't already exist on that card
    * and return either the new checklist or the 
    * checklist that already existed
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} the name of the checklist
    * to copy, can be a string or RegExp, if more than one matches
    * will just copy the first found
    * @param to_card {Card} the card to copy the checklist to
    * var notif = new Notification(posted);
    * //Copy a checklist to a card if it was moved or added to the ToDo list
    * notif.board().card("Templates").copyUniqueChecklist("Some Procedure",notif.addedCard("ToDo"));
    */
    this.copyUniqueChecklist = function(name,to_card,position)
    {
        try
        {
            return to_card.checklist(name);
        }
        
        catch(e)
        {
            Notification.expectException(InvalidDataException,e);
            return this.copyChecklist(name,to_card,position);
        }
    }

    /**
    * Copy a checklist to another card from this card
    * even if it already exists on the target card
    * and return the new checklist
    * @memberof module:TrelloEntities.Card
    * @param name {string|RegExp} the name of the checklist to
    * copy, if multiple matches will just take the first found
    * @param to_card {Card} the card to copy the checklist to
    * @param position {string} (optional) either top or bottom, or a positive number, defaults to bottom
    * @example
    * var notif = new Notification(posted);
    * //Copy a checklist to a card if it was moved or added to the ToDo list
    * notif.board().card("Templates").copyChecklist("Some Procedure",notif.addedCard("ToDo"));
    */
    this.copyChecklist = function(name,to_card,position)
    {
        var ret = new Checklist(TrelloApi.post("cards/"+to_card.id()+"/checklists?idChecklistSource="+this.checklist(name).id())).setContainingCard(to_card);

        //HACK: The post endpoint for adding a checklist to a card ignores the pos parameter
        //so we need a separate put to update the position once added
        if(position)
            ret.setPosition(position);

        this.checklist_list = null;
        to_card.checklist_list = null;
        return ret;
    }

    /**
    * Remove a checklist from this card
    * @memberof module:TrelloEntities.Card
    * @param checklist {Checklist} the checklist to remove
    * @example
    * var card = new Notification(posted).movedCard("Done");
    * card.removeChecklist(card.checklist("Some Process"));
    */
    this.removeChecklist = function(checklist)
    {
        TrelloApi.del("cards/"+this.data.id+"/checklists/"+checklist.data.id);
        this.checklist_list  = null;
        return this;
    }

    /**
    * Add a checklist to this card, or pass an
    * existing checklist into the callback if it already
    * exists on this card
    * @memberof module:TrelloEntities.Card
    * @param name {string} The name of the checklist to add
    * @param callback {Function} a callback which will recieve
    * the new or existing Checklist object to add items to it
    * @param position {string} (optional) top, bottom, a number, defaults
    * to "bottom"
    * @example
    * new Notification(posted).movedCard("ToDo").addChecklist("Do Stuff",function(cl)
    * {
    *     cl.addItem("Did you do this yet?");
    * });
    */
    this.addChecklist = function(name,callback,position)
    {
        try
        {
            var checklist = this.checklist(name);
        }
        
        catch(e)
        {
            //var checklist = new Checklist(TrelloApi.post("cards/"+this.data.id+"/checklists?name="+encodeURIComponent(name)+"&pos="+encodeURIComponent(position))).setContainingCard(this);
            Notification.expectException(InvalidDataException,e);
            var checklist = new Checklist(TrelloApi.post("cards/"+this.data.id+"/checklists?name="+encodeURIComponent(name))).setContainingCard(this);
            this.checklist_list = null;
        }
        
        //HACK: The post endpoint for adding a checklist to a card ignores the pos parameter
        //so we need a separate put to update the position once added
        if(position)
            checklist.setPosition(position);

        if(callback)
            callback(checklist);

        return this;
    }

    /**
    * Remove a label from this card by label object
    * @memberof module:TrelloEntities.Card
    * @param label {Label} a Label object to remove from this card
    * @example
    * var notif = new Notification(posted);
    * notif.card().removeLabel(notif.board().label("Some Label"));
    */
    this.removeLabel = function(label)
    {
        try
        {
            TrelloApi.del("cards/"+this.data.id+"/idLabels/"+label.id());
            this.labels_list = null;
        }

        catch(e)
        {
            Notification.expectException(InvalidRequestException,e);
        }

        return this;
    }

    /**
    * Add a label to the card by name, whether it 
    * already exists in the board or not
    * @memberof module:TrelloEntities.Card
    * @param label_name {string} The name of the label to add
    * @example
    * new Notification(posted).card().addLabel("New");
    */
    this.addLabel = function(label_name)
    {
        try
        {
            var label = this.board().label(label_name);
            this.applyLabelIds(new IterableCollection([label.id()]));
        }
        
        catch(e)
        {
            Notification.expectException(InvalidDataException,e);
            this.addNewLabels(new IterableCollection([label_name]));
        }
        
        this.labels_list = null;
        this.data.labels = null;
        return this;
    }

    /**
    * Add new labels that don't already exist to a card
    * by name. You should just use the addLabel() method instead
    * @memberof module:TrelloEntities.Card
    * @param new_labels {Array} an array of label names to create
    * on this board and added to the card
    */
    this.addNewLabels = function(new_labels)
    {
        new_labels.each(function(label)
        {
            try
            {
                TrelloApi.post("cards/"+this.data.id+"/labels?color=null&name="+encodeURIComponent(label));
            }
            
            catch(e)
            {
            }
        }.bind(this));
        
        this.labels_list = null;
        return this;
    }

    /**
    * Add existing labels to a card by ID. You should just
    * use the addLabel method instead
    * @memberof module:TrelloEntities.Card
    * @param label_ids {Array} an array of label_ids
    */
    this.applyLabelIds = function(label_ids)
    {
        label_ids.each(function(id)
        {
            try
            {
                TrelloApi.post("cards/"+this.data.id+"/idLabels?value="+encodeURIComponent(id));
            }
            
            catch(e)
            {
            }
        }.bind(this));

        this.labels_list = null;
        return this;
    }

    /**
    * Return the value of a custom field by name.
    * This works for all field types, and the value is
    * converted to the appropriate data type based on
    * the type of field (eg. number/date/etc)
    *
    * If the Custom Fields power up is not enabled
    * this method will attempt to enable it. If it can't
    * be enabled because the board has reached the power up
    * limit, an unexpected exception is thrown.
    *
    * If a field with the given name doesn't exist it will 
    * be created as a text type field
    * @memberof module:TrelloEntities.Card
    * @param field_name {striung} the name of the field to get the value for on this card
    * @example
    * new Notification(posted).card().customFieldValue("My Field");
    */
    this.customFieldValue = function(field_name)
    {
        var field = this.findOrCreateCustomFieldFromName(field_name);
        var ret = false;

        this.customFields().each(function(loop)
        {
            if(loop.idCustomField == field.id)
            {
                ret = this.extractCustomFieldValueFromDataBasedOnType(loop);
            }
        }.bind(this));
        
        return ret;
    }

    //INTERNAL USE ONLY
    this.extractCustomFieldValueFromDataBasedOnType = function(data)
    {
        var ret = "";

        if(data.value)
        {
            for(var key in data.value)
            {
                switch(key)
                {
                    case "text":
                        ret = data.value[key];
                    break;
                      
                    case "number":
                        ret = parseFloat(data.value[key]);
                    break;
                      
                    case "checked":
                        ret = (data.value[key] === 'true') ? true:false;
                    break;
                      
                    case "date":
                        ret = new Date(data.value[key]);
                    break;
                      
                    default:
                        ret = "";
                    break;
                }
            }
        }
        
        else if(data.idValue)
        {
            new IterableCollection(TrelloApi.get("customField/"+data.idCustomField+"/options")).each(function(option)
            {
                if(data.idValue == option._id)
                    ret = option.value.text;
            });

        }
        
        return ret;
    }

    /**
    * Get an IterableCollection of all custom fields
    * as raw objects for this card (not really that useful)
    * @memberof module:TrelloEntities.Card
    */
    this.customFields = function()
    {
        return new IterableCollection(TrelloApi.get("cards/"+this.id()+"/customFieldItems"));
    }

    /**
    * Set the value of a custom field. Automatically converts
    * the value to the correct type for the API, including
    * looking up dropdown options etc. 
    *
    * If the Custom Fields power up is not enabled
    * this method will attempt to enable it. If it can't
    * be enabled because the board has reached the power up
    * limit, an unexpected exception is thrown.
    *
    * If a field with the given name doesn't exist it will 
    * be created as a text type field.
    * @memberof module:TrelloEntities.Card
    * @param field_name {string} the name of the field to get the value for on this card
    * @param field_value {string|Date|int|float|double|boolean} the value to set
    * @example
    * new Notification(posted).card().setCustomFieldValue("My Field","Hi there");
    */
    this.setCustomFieldValue = function(field_name,value)
    {
        var field = this.findOrCreateCustomFieldFromName(field_name);
        var url = "https://api.trello.com/1/card/"+this.id()+"/customField/"+field.id+"/item";
        
        var payload = {
            key: TrelloApi.checkControlValues().key,
            token: TrelloApi.checkControlValues().token
          };

        this.insertTrelloCustomFieldValue(payload,value,field);
        HttpApi.call("put",url,"",{"content-type": "application/json"},JSON.stringify(payload));
        return this;
    }

    //INTERNAL USE ONLY
    this.findOrCreateCustomFieldFromName = function(field_name)
    {
        return this.board().findOrCreateCustomFieldFromName(field_name);
    }

    //INTERNAL USE ONLY
    this.insertTrelloCustomFieldValue = function(payload,value,field)
    {
      //Clear the field if the value is empty
      if ( value === "" || value === null || value === undefined ) {
        payload.value = "";
      }
      
      else
      {
        
        switch (field.type)
        {
          case "text":
            payload.value = { text: value.toString() };
            break;
            
          case "number":
            var n = parseFloat(value);
            if ( isNaN(n) ) {
              payload.value = "";
            } else {
              payload.value = {number: n.toString() };
            }
            break;
            
          case "checkbox":
            payload.value = {checked: (!!value).toString() };
            break;
            
          case "date":
            var d = new Date(value);
            if ( isNaN( d.getTime() ) ) {
              payload.value = "";
            } else {
              payload.value = { date: d.toISOString() };
            }
            break;
            
          case "list":
            payload.idValue = "";
            
            new IterableCollection(field.options).each(function(opt)
                                                       {
                                                         if(opt.value.text == value)
                                                           payload.idValue = opt.id;
                                                       });
            break;
            
          default:
            //This shouldn't happen. We can't assume the type, so we clear the field instead.
            payload.value = "";
            break;
        }
      }
    }
    
    /**
    * Add a custom or default sticker.
    * @param sticker {string} To add a custom sticker, pass in the ID of a custom
    * sticker, fetched with Member.customStickers, for
    * example new Trellinator().customStickers() if your 
    * custom sticker list was created by your Trellinator
    * user. Note that custom stickers are only avialable in business/enterprise
    * class accounts. Otherwise you can pass in a predefined sticker from this list:
    * 
    * Standard stickers (available in free accounts):
    * - check
    * - heart
    * - warning
    * - clock
    * - smile
    * - laugh
    * - huh
    * - frown
    * - thumbsup
    * - thumbsdown
    * - star
    * - rocketship
    * 
    * Premium stickers (business/enterprise class only):
    * - taco-love
    * - taco-confused
    * - taco-cool
    * - taco-angry
    * - taco-celebrate
    * - taco-robot
    * - taco-alert
    * - taco-active
    * - taco-money
    * - taco-reading
    * - taco-trophy
    * - taco-sleeping
    * - taco-pixel
    * - taco-proto
    * - taco-embarrassed
    * - taco-clean
    * - pete-happy
    * - pete-love
    * - pete-broken
    * - pete-alert
    * - pete-talk
    * - pete-vacation
    * - pete-confused
    * - pete-shipped
    * - pete-busy
    * - pete-completed
    * - pete-space
    * - pete-sketch
    * - pete-ghost
    * - pete-award
    * - pete-music
    * @param top {int} (optional) y co-ordinate between -60 and 100 default 0
    * @param left {int} (optional) x co-ordinate between -60 and 100 default 0 
    * @param rotate {int} (optional) degree of rotation between 0 and 360 default 0
    * @param z {int} (optional) z-index (ie. if this should sit "on top" of other stickers) 0 is highest, ie. "on top" of everything else, and is the default
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).card().addSticker("pete-happy");
    */
    this.addSticker = function(sticker,top,left,rotate,z)
    {
        if(!top)
            top = 0;
        if(!left)
            left = 0;
        if(!rotate)
            rotate = 0;
        if(!z)
            z = 10;

        TrelloApi.post("cards/"+this.id()+"/stickers/?image="+encodeURIComponent(sticker)+"&top="+parseInt(top)+"&left="+parseInt(left)+"&rotate="+parseInt(rotate)+"&zIndex="+parseInt(z));
        return this;
    }
    
    /**
    * Remove all stickers from a card
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).card().removeAllStickers();
    */
    this.removeAllStickers = function()
    {
        new IterableCollection(TrelloApi.get("cards/"+this.id()+"/stickers")).each(function(sticker)
        {
            TrelloApi.del("cards/"+this.id()+"/stickers/"+sticker.id);
        }.bind(this));
        return this;
    }

    /**
    * Reset cached objects and load data from Trello.
    * You may find this is required sometimes to force
    * a reload of an object, but you shouldn't use this
    * habitually.
    * @memberof module:TrelloEntities.Card
    * @example
    * new Notification(posted).card().load().currentList();
    */
    this.load = function()
    {
        this.checklist_list  = null;
        this.labels_list     = null;
        this.members_list    = null;
        this.current_list = null;
        this.moved_to_list_cache = null;
        this.containing_board = null;
        var attempt = this.data.id;
        this.data = TrelloApi.get("cards/"+this.data.id+"?fields=all&actions=all&attachments=true&attachment_fields=all&members=true&member_fields=all&memberVoted_fields=all&checklists=all&checklist_fields=all&board=true&board_fields=all&list=true&pluginData=true&stickers=true&sticker_fields=all");
      

        if(!this.data)
            throw new InvalidDataException("Unable to load card with id: "+attempt);

        return this;
    }


    //DEPRECATED
    this.completeAllItemsOnChecklist = function(name)
    {
        this.checklist(name).markAllItemsComplete();
        return this;
    }

    
    if(!this.data.id && this.data.link)
    {
        this.data.id = TrelloApi.cardLinkRegExp().exec(this.data.link)[1];
        this.load();
    }
}

/**
* Create a new card in the given list with the
* given name, or key/value pairs in an object, using
* parameters from {@link https://developers.trello.com/reference/#cards-2}
* @memberof module:TrelloEntities.Card
* @param list {List} a List to add the card to 
* @param data {string|Object} either a card name to use 
* or an Object of key/value pairs
* @example
* Card.create(new Trellinator().board("Some Board").list("ToDo"),"Hi there!");
* @example
* Card.create(new Trellinator().board("Some Board").list("ToDo"),{name: "Hi there!",pos:"top"});
*/
Card.create = function(list,data,pos)
{
  if(typeof data === "string")
  {
    data = {name: data};
    
    if(!pos)
        pos = "top";
    
    data.pos = pos;
  }

  else if(data && data.desc)
  {
      if(data.desc.length > 16384)
          data.desc = data.desc.substring(0,16381)+"...";
  }
  
  var ret = new Card(TrelloApi.post("cards?idList="+list.id()+"&"+new IterableCollection(data).implode("&",encodeURIComponent)));
  list.card_list = null;
  return ret;
}

/**
* Find a card or create it if it doesn't already with
* either just a string name, or an Object with key/value
* pairs from {@link https://developers.trello.com/reference/#cards-2}
* exist, in the given list. The card can exist anywhere
* on the same board as the target list, but will be created
* in the target list if it doesn't exist.
* @memberof module:TrelloEntities.Card
* @param list {List} a List object to find or create the card in
* @param data {string|Object} either the name of the card, or an Object
* with at least a name attribute to be used to find the card, and then
* data to be used when creating the card
* @example
* Card.findOrCreate(new Trellinator().board("My Board").list("Inbox"),"New Card Name");
*/
Card.findOrCreate = function(list,data)
{
    try
    {
        var ret = list.board().card(data);
    }
    
    catch(e)
    {
        Notification.expectException(InvalidDataException,e);
        var ret = Card.create(list,data);
    }
    
    return ret;
}