From 4d15dfa9c9c78c6c0b3bc8788d376408e0c338ff Mon Sep 17 00:00:00 2001 From: monknomo Date: Sun, 15 Mar 2015 16:00:41 -0800 Subject: [PATCH] significant refactor for unit tests --- .gitignore | 3 +- bootstrap_template.html | 29 -- cardwiki/__init__.py | 295 +++++++++++++++ cardwiki/db/__init__.py | 279 +++++++------- cardwiki/test/__init__.py | 538 ++++++++++++++++++++++++++ cardwiki/views.py | 323 ++++------------ cardwiki/wikilinks/__init__.py | 8 +- index.html | 634 +++---------------------------- init_db.py | 42 +++ static/css/custom.css | 8 + static/css/test.css | 3 + static/js/cardwiki.js | 396 +++++++++++++++++++ static/js/jquery.tagsinput2.js | 364 ++++++++++++++++++ static/test/tests.js | 667 +++++++++++++++++++++++++++++++++ test.html | 23 ++ 15 files changed, 2595 insertions(+), 1017 deletions(-) delete mode 100644 bootstrap_template.html create mode 100644 cardwiki/test/__init__.py create mode 100644 init_db.py create mode 100644 static/css/test.css create mode 100644 static/js/cardwiki.js create mode 100644 static/js/jquery.tagsinput2.js create mode 100644 static/test/tests.js create mode 100644 test.html diff --git a/.gitignore b/.gitignore index 697c911..cb44655 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,5 @@ Temporary Items # compiled python files *.pyc # sqlite database file -*.db \ No newline at end of file +*.db +*.txt \ No newline at end of file diff --git a/bootstrap_template.html b/bootstrap_template.html deleted file mode 100644 index e448828..0000000 --- a/bootstrap_template.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - Bootstrap 101 Template - - - - - - - - - - - - - - -

Hello, world!

- - - - \ No newline at end of file diff --git a/cardwiki/__init__.py b/cardwiki/__init__.py index 139597f..5e68765 100644 --- a/cardwiki/__init__.py +++ b/cardwiki/__init__.py @@ -1,2 +1,297 @@ +"""cardwiki module +This module is container for manipulating the parts of a cardwiki and interacting +with the cardwiki database in a predictable and predefined way +""" +from sqlalchemy import func, and_, exists +from sqlalchemy.orm.exc import NoResultFound +import markdown +from passlib.hash import bcrypt +from sqlalchemy.orm.exc import NoResultFound as SqlAlchemyNoResultFound +import cardwiki.db as db +import re +import datetime +import copy + +BASE_URL = "/" + +class CardNotFoundException(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +def get_cards(session): + """Returns a list of cards in the database + + Args: + session (sqlalchemy session): a session with the database + + Returns: + []: A list of dictonaries representing all the cards contained in the + database. + """ + result = [] + for card in session.query(db.Card): + result.append({'display_title':card.display_title, + 'link':card.link, + 'current_version':len(card.versions.all())}) + return result + +def derive_title_link(title): + """Returns a cardwiki-style link; replaces spaces with underscores and non-html + safe characters with an empty string. + + Args: + title (str): A title of a card + + Returns: + str: A modified title, substituting underscores for spaces and removing + non-html friendly characters + """ + title = re.sub(r'[\ ]', '_', title) + title = re.sub(r'[^a-zA-Z0-9_~\-\.]', '', title) + return title + +def get_newest_card(link, session): + """Returns a dictionary representing the newest card for a given link + + Args: + link (str): A link to a card + session (sqlalchemy session): a session with the database + + Returns: + dict: A dictionary represenation of the newest version of the card corresponding + with the link + """ + try: + newest_card = session.query(db.Card).filter(db.Card.link == link).one() + return newest_card.to_dict() + except NoResultFound: + return None + +def get_card_version(link, version, session): + cards = session.query(db.Card).filter(db.Card.link == link).one() + try: + card = cards.versions[version-1] + value = card.to_dict() + value = copy.deepcopy(value) + value['version'] = version + return value + except IndexError: + return None + +def insert_card(card, session): + """Inserts a card into the database + + Args: + card (dict): A dictionary representing a card + session (sqlalchemy session): a session with the database + + Returns: + dict: a dictionary representing the card after insertion into the database + """ + new_card = db.Card(display_title=card['display_title'], + link=card['link'], + content=card['content'], + rendered_content=card['rendered_content'], + edited_by=card['edited_by']) + session.add(new_card) + session.flush() + + result = new_card.to_dict() + result['version'] = new_card.versions.count() + 1 + return result + +def delete_card(link, session): + """Deletes a card in the database + + Args: + link (str): A link to a card + session (sqlalchemy session): a session with the database + """ + if session.query(db.Card).filter(db.Card.link == link).count() > 0: + for card in session.query(db.Card).filter(db.Card.link == link).all(): + session.delete(card) + else: + value = "Tried to delete {0}, but it was not there".format(link) + e = CardNotFoundException(value) + print(e) + raise e + +def get_tags_for_card(link, session): + """Gets all tags for a link to a card + + Args: + link (str): A link to a card + session (sqlalchemy session): a session with the database + + Returns: + list: A list of tags corresponding to the card at the link + """ + query = session.query(db.CardTag) + try: + target_card = session.query(db.Card).filter(db.Card.link == link).one() + target_card = target_card.id + except SqlAlchemyNoResultFound: + value = "Tried to find tags for card {0}, but found no card".format(link) + e = CardNotFoundException(value) + raise e + query = query.filter(db.CardTag.tagged_card == target_card) + results = {"tags":[]} + for tag in query: + results["tags"].append({"tag":tag.tag, + "href":"{0}tags/{1}".format(BASE_URL, + tag.tag)}) + return results + +def request_to_carddict(request): + """Converts a bottle request to a dictionary representing a card + + Args: + request (bottle.request): A bottle request with json corresponding to a cardwiki card + + Returns: + dict: a dictionary representing a card + """ + formatted_url = ['wikilinks(base_url={0}cards/)'.format(BASE_URL)] + rendered_content = markdown.markdown(request.json['content'], formatted_url) + try: + version = request.json['version'] + return {'link': request.json['link'], + 'display_title': request.json['display_title'].strip(), + 'version': version, + 'content': request.json['content'], + 'rendered_content': rendered_content, + 'edited_by': request.json['edited_by']} + except KeyError as keyerror: + return {'link': request.json['link'], + 'display_title': request.json['display_title'].strip(), + 'version': 1, + 'content': request.json['content'], + 'rendered_content': rendered_content, + 'edited_by': request.json['edited_by']} + +def insert_tags(tags, session): + """Inserts a list of tag dictionaries into the database + + Args: + tags (list): A list of tags, where each tag is a dict with a 'tag' key + and a 'tagged_card' key + session (sqlalchemy session): a session with the database + + Returns: + list: A list of dictionaries representations of tags + """ + inserted_tags = [] + for tag in tags: + try: + target_card = session.query(db.Card).filter(db.Card.link == tag['tagged_card']).one() + card_id = target_card.id + except SqlAlchemyNoResultFound: + value = "Tried to inserting tags for card {0}, but found no card".format(tag['tagged_card']) + raise CardNotFoundException(value) + split_tags = re.split(r"[:,;+\.| ]", tag["tag"]) + + for split_tag in split_tags: + if split_tag.strip() != "": + query = session.query(db.CardTag) + query = query.filter(db.CardTag.tagged_card == + card_id, + db.CardTag.tag == split_tag) + if query.count() == 0: + print("inserting tag") + tag_to_insert = db.CardTag(tagged_card=card_id, + tag=split_tag) + session.add(tag_to_insert) + inserted_tags.append(tag_to_insert.to_dict()) + return tags + +def delete_tag(tag, session): + """Deletes a tag + + Args: + tag (dict): a dict with a 'tag' key and a 'tagged_card' key + session (sqlalchemy session): a session with the database + """ + try: + target_card = session.query(db.Card).filter(db.Card.link == tag['tagged_card']).one() + card_id = target_card.id + except SqlAlchemyNoResultFound: + value = "Tried deleting tag {0} for card {1}, but found no card".format(tag['tag'], tag['tagged_card']) + raise CardNotFoundException(value) + tag_to_be_deleted = session.query(db.CardTag) + tag_to_be_deleted = tag_to_be_deleted.filter(db.CardTag.tag == tag['tag'], + db.CardTag.tagged_card == + card_id) + for dtag in tag_to_be_deleted: + session.delete(dtag) + + + +def find_all_tags(session): + """Finds all the tags in the database + + Args: + session (sqlalchemy session): a session with the database + + Returns: + dict: A dictionary containing a list of tag dictionaries + """ + query = session.query(db.CardTag.tag, func.count(db.CardTag.tag)) + query = query.group_by(db.CardTag.tag).all() + result = {"tags":[]} + for tag in query: + href = '{0}tags/{1}'.format(BASE_URL, tag[0].replace(" ", "_")) + result["tags"].append({"tag":tag[0], "count":tag[1], "href":href}) + return result + +def find_cards_for_tag(tag, session): + """Finds all the cards corresponding to a tag + + Args: + tag (str): A tag + session (sqlalchemy session): a session with the database + """ + tags = session.query(db.CardTag.tagged_card).filter(db.CardTag.tag == tag) + cards = session.query(db.Card).filter(db.Card.id.in_(tags)).all() + result = {"cards":[]} + for card in cards: + card_dict = card.to_dict() + card_dict['href'] = '{0}cards/{1}'.format(BASE_URL, card_dict['link']) + result["cards"].append(card_dict) + return result + +def perform_login(username, password, request_url, session): + """Attempts to authenticate a user for an initial login. Each REST resource + that requires authentication requires a username and password be sent, and + this method determines whether authentication suceeds or fails + + Args: + username (str): a username + password (str): a plain text password + request_url (str): the requested resources url + session (sqlalchemy session): a session with the database + + Returns: + dict: containing the key 'authentication_status' with the values 'success' + or 'failure'. If the value of 'authentication_status' is 'failure', also + contains 'request_url' with the path of the requested resource and 'reason' + with a reason for failure. + """ + try: + user = session.query(db.User).filter(db.User.username == username).one() + if bcrypt.verify(password, user.passwordhash): + user.last_seen = datetime.datetime.utcnow() + session.add(user) + return {'authentication_status':"success", + 'request_url':request_url} + else: + return {"authentication_status":"failure", + "request_url":request_url, + "reason":"We don't recognize your username with that password"} + except NoResultFound: + return {"authentication_status":"failure", + 'reason':'User not found', + 'request_url':request_url} diff --git a/cardwiki/db/__init__.py b/cardwiki/db/__init__.py index 3dba23f..6749ad5 100644 --- a/cardwiki/db/__init__.py +++ b/cardwiki/db/__init__.py @@ -1,21 +1,29 @@ +""" +Database objects and access definitions for cardwiki +""" + from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import create_engine, Column, Integer, String, Boolean, Text, Sequence, ForeignKey, Date, DateTime, ForeignKeyConstraint -from sqlalchemy.orm import relationship, sessionmaker +from sqlalchemy import create_engine, Column, Integer, String, Boolean, Text +from sqlalchemy import Sequence, ForeignKey, Date, DateTime +from sqlalchemy import ForeignKeyConstraint, UniqueConstraint +from sqlalchemy.orm import relationship, sessionmaker, backref, configure_mappers +from sqlalchemy_continuum import make_versioned, version_class from passlib.hash import bcrypt from contextlib import contextmanager +import datetime as dt +import re -db_path = "wiki.db" - -engine = create_engine('sqlite:///{0}'.format(db_path), echo=True) -_Session = sessionmaker(bind=engine) -#Session = sessionmaker +make_versioned(user_cls=None) -Base = declarative_base() +DB_PATH = "wiki.db" +ENGINE = create_engine('sqlite:///{0}'.format(DB_PATH), echo=False) +BASE = declarative_base() @contextmanager def session_scope(): """Provide a transactional scope around a series of operations.""" - session = _Session() + + session = sessionmaker(bind=ENGINE)() try: yield session session.commit() @@ -25,164 +33,145 @@ def session_scope(): finally: session.close() -class Card(Base): +def derive_title_link(title): + """Derives a title sutiable for linking to""" + title = re.sub(r'[\ ]', '_', title) + title = re.sub(r'[^a-zA-Z0-9_~\-\.]', '', title) + return title + +class Card(BASE): + """A Card object for persistence""" + __versioned__ = {} __tablename__ = 'card' - linkable_title = Column(String(200), primary_key=True) - version = Column(Integer, primary_key=True) - title = Column(String(200), nullable=False) + + id = Column(Integer, primary_key=True, autoincrement=True) + display_title = Column(String(200), nullable=False) + link = Column(String(200), nullable=False) content = Column(Text()) rendered_content = Column(Text()) - edited_at = Column(DateTime()) - edited_by = Column(Integer, ForeignKey('user.id')) - previous_title = Column(String(200)) - next_title = Column(String(200)) - wikilinks = relationship("CardWikiLink", backref="card", primaryjoin='and_(Card.title==CardWikiLink.from_card, Card.version == CardWikiLink.from_card_version)') + edited_at = Column(DateTime(), default=dt.datetime.utcnow()) + edited_by = Column(Integer, ForeignKey('user.id'), nullable=False) tags = relationship("CardTag", backref="card") - + + + def __repr__(self): - return "".format(self.linkable_title, - self.title, - self.version, - self.content, - self.rendered_content, - self.edited_at, - self.edited_by) - + """returns a repr of this Card""" + return "".format(self.id, + self.display_title, + self.link, + self.content, + self.rendered_content, + self.edited_at, + self.edited_by) + def to_dict(self): - if self.edited_at is None: - edited_at = None - else: - edited_at = self.edited_at.isoformat() - return {"title":self.title, - "linkable_title":self.linkable_title, - "version":self.version, + """returns a dictionary of this card, suitable for json serializing""" + return {"id":self.id, + "display_title":self.display_title, + "link":self.link, "content":self.content, "rendered_content":self.rendered_content, - "edited_at":edited_at, + "edited_at":self.edited_at.isoformat(), "edited_by":self.edited_by} + + -class CardWikiLink(Base): - __tablename__ = 'card_wikilink' - from_card = Column(String(200), ForeignKey('card.linkable_title'), primary_key=True) - from_card_version = Column(Integer, ForeignKey('card.version'), primary_key=True) - to_card = Column(String(200), ForeignKey('card.linkable_title'), primary_key=True) - - ForeignKeyConstraint(['from_card', 'from_card_version'], ['Card.linkable_title', 'Card.version']) - - def __repr__(self): - return ''.format(self.from_card, - self.to_card) - def to_dict(self): - return {"from_card":self.from_card, - "from_card_version":self.from_card_version, - "to_card":self.to_card} - -class CardTag(Base): +class CardTag(BASE): + """An object for relating tags to Cards, and persisting that relationship""" __tablename__ = 'card_tag' - tagged_card = Column(String(200), ForeignKey('card.linkable_title'), primary_key=True) + tagged_card = Column(Integer, + ForeignKey('card.id'), + primary_key=True) tag = Column(String, primary_key=True) - + def __repr__(self): - return "".format(self.tagged_card, - self.tag) - + """returns a repr of this CardTag""" + return "".format(self.tagged_card, + self.tag) + def to_dict(self): + """return a dictionary of this CardTag suitable for serializing to json""" return {"tagged_card":self.tagged_card, "tag":self.tag} -class User(Base): +class User(BASE): + """An object for persisting users""" - def __init__ (self, *args, **kwargs): - if kwargs['plainpassword'] is not None: - kwargs['passwordhash']=bcrypt.encrypt(kwargs['plainpassword']) - kwargs.pop('plainpassword') - super( User, self ).__init__(**kwargs) - - - __tablename__ = 'user' id = Column(Integer, Sequence('user_id_seq'), primary_key=True) - username = Column(String(50)) + username = Column(String(254)) google_id = Column(String(100)) github_id = Column(String(100)) facebook_id = Column(String(100)) twitter_id = Column(String(100)) yahoo_id = Column(String(100)) passwordhash = Column(String(5000)) - last_seen = Column(DateTime()) - - repositories = relationship("UserRepository", backref="user") + last_seen = Column(DateTime(), default=dt.datetime.utcnow()) bio = relationship("UserBiography", uselist=False, backref="user") - + cards = relationship("Card", backref="card") + + def __init__(self, **kwargs): + """A custom init method that allows passing in of plaintext passords + to ensure that they are hashed with the preferred bcrypt algorithm""" + if kwargs['plainpassword'] is not None: + kwargs['passwordhash'] = bcrypt.encrypt(kwargs['plainpassword']) + kwargs.pop('plainpassword') + super(User, self).__init__(**kwargs) + def to_dict_dangerous(self): + """returns a dictionary of this User, suitable for serializing to json. + Dangerous because it includes the password hash. Don't just give this back""" if self.last_seen is None: last_seen = None else: last_seen = self.last_seen.isoformat() return {"id":self.id, - "username":self.username, - "passwordhash":self.passwordhash, - "google_id":self.google_id, - "github_id":self.github_id, - "facebook_id":self.facebook_id, - "twitter_id":self.twitter_id, - "yahoo_id":self.yahoo_id, - "last_seen":last_seen} - + "username":self.username, + "passwordhash":self.passwordhash, + "google_id":self.google_id, + "github_id":self.github_id, + "facebook_id":self.facebook_id, + "twitter_id":self.twitter_id, + "yahoo_id":self.yahoo_id, + "last_seen":last_seen} + def to_dict(self): + """Returns a dictionary of this User, suitable for serializing to json. + No password included, safe to pass around""" if self.last_seen is None: last_seen = None else: last_seen = self.last_seen.isoformat() return {"id":self.id, - "username":self.username, - "google_id":self.google_id, - "github_id":self.github_id, - "facebook_id":self.facebook_id, - "twitter_id":self.twitter_id, - "yahoo_id":self.yahoo_id, - "last_seen":last_seen} - - + "username":self.username, + "google_id":self.google_id, + "github_id":self.github_id, + "facebook_id":self.facebook_id, + "twitter_id":self.twitter_id, + "yahoo_id":self.yahoo_id, + "last_seen":last_seen} + + def __repr__(self): - return """""".format(self.username, - self.google_id, - self.github_id, - self.facebook_id, - self.twitter_id, - self.yahoo_id, - self.passwordhash, - self.salt) - -class UserRepository(Base): - __tablename__ = 'user_repository' - id = Column(Integer, Sequence('user_repository_seq'), primary_key=True) - user_id = Column(Integer, ForeignKey('user.id')) - name = Column(String(1000)) - is_website = Column(Boolean()) - - def to_dict(self): - return {"id":self.id, - "user_id":self.user_id, - "name":self.name, - "is_website":self.is_website} - - def __repr__(self): - return "".format(self.user_id, - self.name, - self.is_website) - -class UserBiography(Base): + google_id='{1}', + github_id='{2}', + facebook_id='{3}', + twitter_id='{4}', + yahoo_id='{5}')>""".format(self.username, + self.google_id, + self.github_id, + self.facebook_id, + self.twitter_id, + self.yahoo_id, + self.passwordhash) + +class UserBiography(BASE): + """An object to allow users to persist a little something about themselves""" __tablename__ = 'user_biography' user_id = Column(Integer, ForeignKey('user.id'), primary_key=True) name = Column(String(500)) @@ -192,8 +181,10 @@ class UserBiography(Base): phone = Column(String(20)) backup_phone = Column(String(20)) bio = Column(String(2000)) - + def to_dict(self): + """returns a dictionary representation of this UserBiography suitable + for serializing to json""" return {"user_id":self.user_id, "name":self.name, "birthday":self.birthday.isoformat(), @@ -202,15 +193,31 @@ def to_dict(self): "phone":self.phone, "backup_phone":self.backup_phone, "bio":self.bio} - + def __repr__(self): + """returns a repr for this UserBiography""" return "".format(self.user_id, - self.name, - self.birthday, - self.email, - self.backup_email, - self.phone, - self.backup_phone, - self.bio) + self.name, + self.birthday, + self.email, + self.backup_email, + self.phone, + self.backup_phone, + self.bio) + + +configure_mappers() +CardVersion = version_class(Card) + +def _card_version_to_dict(class_): + return {"id":class_.id, + "display_title":class_.display_title, + "link":class_.link, + "content":class_.content, + "rendered_content":class_.rendered_content, + "edited_at":class_.edited_at.isoformat(), + "edited_by":class_.edited_by} + +CardVersion.to_dict = _card_version_to_dict \ No newline at end of file diff --git a/cardwiki/test/__init__.py b/cardwiki/test/__init__.py new file mode 100644 index 0000000..10f16e9 --- /dev/null +++ b/cardwiki/test/__init__.py @@ -0,0 +1,538 @@ +import cardwiki +from cardwiki.db import session_scope +from cardwiki import views +from sqlalchemy.orm.exc import NoResultFound +import unittest +import copy + +class MockCardRequest: + def __init__(self): + self.json = {'link': "json_test_title", + 'display_title': "json test title", + 'version': "4", + 'content': "test content", + 'rendered_content': "

test content

", + 'edited_by': "unittest"} + +class MockTagRequest: + def __init__(self): + self.json = {'tags':[{'tag':'descriptive_tag', + 'tagged_card':'__startCard'}]} + +class TestCardwikiFunctions(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_get_cards(self): + with session_scope() as session: + cards = cardwiki.get_cards(session) + print(cards) + self.assertEqual('', cards[0]['display_title']) + self.assertEqual('__startCard', cards[0]['link']) + self.assertEqual(1, cards[0]['current_version']) + + def test_get_tags_for_card(self): + tags = {} + with session_scope() as session: + tags = cardwiki.get_tags_for_card('__startCard', session) + self.assertEqual(1, len(tags['tags'])) + self.assertEqual('administrivia', tags['tags'][0]['tag']) + self.assertEqual('/tags/administrivia', tags['tags'][0]['href']) + + def test_get_newest_card(self): + card = {} + with session_scope() as session: + card = cardwiki.get_newest_card('__startCard', session) + self.assertEqual('__startCard', card['link']) + self.assertEqual('', card['display_title']) + + def test_get_newest_card_nonexistent(self): + card = None + with session_scope() as session: + card = cardwiki.get_newest_card('foobar', session) + self.assertIsNone(card) + + def test_insert_card(self): + try: + with session_scope() as session: + initial_card = {'display_title' : "test card", + 'link':"test_card", + 'content':"### test content", + 'rendered_content': + "

test content

", + 'edited_by':"unittest"} + card = cardwiki.insert_card(initial_card, session) + self.assertIsNotNone(card['id']) + self.assertEqual(initial_card['display_title'], card['display_title']) + self.assertEqual(initial_card['link'], card['link']) + self.assertEqual(initial_card['content'], card['content']) + self.assertEqual(initial_card['rendered_content'], card['rendered_content']) + self.assertEqual(initial_card['edited_by'], card['edited_by']) + finally: + with session_scope() as session: + cardwiki.delete_card('test_card', session) + + def test_delete_card(self): + card = {} + with session_scope() as session: + initial_card = {'display_title' : "test card", + 'link':"test_card", + 'content':"### test content", + 'rendered_content': + "

test content

", + 'edited_by':"admin"} + card = cardwiki.insert_card(initial_card, session) + self.assertIsNotNone(card['id']) + cardwiki.delete_card(card['link'], session) + card = cardwiki.get_newest_card(card['link'], session) + self.assertIsNone(card) + + def test_delete_card_nonexistent(self): + card = None + try: + with session_scope() as session: + cardwiki.delete_card('foobar', session) + self.fail() + except NoResultFound as a: + pass + except: + self.fail() + + def test_request_to_carddict(self): + request = MockCardRequest() + carddict = cardwiki.request_to_carddict(request) + self.assertEqual(request.json, carddict) + + def test_request_to_carddict_no_version(self): + request = MockCardRequest() + request.json.pop("version", None) + carddict = cardwiki.request_to_carddict(request) + request.json['version'] = 1 + self.assertEqual(request.json, carddict) + + def test_insert_tags(self): + tags = [] + tags.append({"tag":"interesting", "tagged_card":1}) + tags.append({"tag":"admin", "tagged_card":1}) + try: + with session_scope() as session: + cardwiki.insert_tags(tags, session) + session.flush() + for tag in tags: + cards = cardwiki.find_cards_for_tag(tag['tag'], session) + self.assertEqual(1, len(cards['cards'])) + card = cards['cards'][0] + self.assertEqual('/cards/__startCard', card['href']) + finally: + with session_scope() as session: + for tag in tags: + cardwiki.delete_tag(tag, session) + + def test_delete_tag(self): + tags = [] + tags.append({"tag":"interesting", "tagged_card":1}) + tags.append({"tag":"admin", "tagged_card":1}) + try: + with session_scope() as session: + cardwiki.insert_tags(tags, session) + with session_scope() as session: + for tag in tags: + cards = cardwiki.find_cards_for_tag(tag['tag'], session) + self.assertEqual(1, len(cards['cards'])) + card = cards['cards'][0] + self.assertEqual('/cards/__startCard', card['href']) + cardwiki.delete_tag(tag, session) + session.flush() + cards = cardwiki.find_cards_for_tag(tag['tag'], session) + self.assertEqual(0, len(cards['cards'])) + finally: + with session_scope() as session: + if len(cardwiki.find_all_tags(session)['tags']) > 1: + for tag in tags: + cardwiki.delete_tag(tag, session) + + def test_delete_spurious_tag(self): + try: + with session_scope() as session: + cardwiki.delete_tag({'tag':'foobar','tagged_card':'goocar'}, session) + self.fail() + except NoResultFound: + pass + + def test_find_all_tags(self): + with session_scope() as session: + tags = cardwiki.find_all_tags(session) + self.assertEqual(1, len(tags['tags'])) + self.assertEqual('administrivia', tags['tags'][0]['tag']) + self.assertEqual(1, tags['tags'][0]['count']) + self.assertEqual('/tags/administrivia', tags['tags'][0]['href']) + + def test_perform_login(self): + with session_scope() as session: + result = cardwiki.perform_login('admin', 'admin', '/', session) + self.assertEqual({'authentication_status':"success", + 'request_url':'/'}, result) + + def test_perform_login_bad_password(self): + with session_scope() as session: + result = cardwiki.perform_login('admin', 'bogus', '/', session) + self.assertEqual({"authentication_status":"failure", + "request_url":'/', + "reason":"We don't recognize your username with "\ + "that password"}, result) + + def test_perform_login_bad_username(self): + with session_scope() as session: + result = cardwiki.perform_login('bogus', 'admin', '/', session) + self.assertEqual({"authentication_status":"failure", + 'reason':'User not found', + 'request_url':'/' }, result) + +class TestCardWikiViews(unittest.TestCase): + def setUp(self): + self.__startCard = {'edited_by':'admin', + 'content':'''Welcome to *Card Wiki*\n========================\n\nModify this card, or add new cards to get started.\nCheck out our documentation at our websites\n\nUse [[wikilinks]] to make new cards''', + 'rendered_content':'''

Welcome to Card Wiki

\n

Modify this card, or add new cards to get started.\nCheck out our documentation at our websites

\n

Use wikilinks to make new cards

''', + 'display_title':'', + 'link':'__startCard', + 'id':1, + 'edited_at':'2015-02-08T10:33:34.573496'} + + def tearDown(self): + pass + + def test_get_index(self): + self.assertTrue(views.get_index().body.name.endswith('index.html')) + + def test_get_index_twice(self): + first = views.get_index().body.name.endswith('index.html') + second = views.get_index().body.name.endswith('index.html') + self.assertEqual(first, second) + + def test_get_static(self): + self.assertTrue(views.get_static('js/validator.min.js').body.name.endswith('validator.min.js')) + + def test_get_static_twice(self): + first = views.get_static('js/validator.min.js').body.name.endswith('validator.min.js') + second = views.get_static('js/validator.min.js').body.name.endswith('validator.min.js') + self.assertEqual(first, second) + + def test_get_static_bogus(self): + with self.assertRaises(bottle.HTTPError, views.get_static, 'js/validator.min.js.bogus') as hopefully_404: + self.assertEqual(404, hopefully_404.status) + + def test_get_all_cards(self): + self.assertEqual(1, len(views.get_all_cards()['cards'])) + + def test_get_all_cards_twice(self): + first = views.get_all_cards() + second = views.get_all_cards() + self.assertEqual(first, second) + + def test_get_card(self): + self.assertEqual(self.__startCard, views.get_card('__startCard')) + + def test_get_card_twice(self): + first = views.get_card('__startCard') + second = views.get_card('__startCard') + self.assertEqual(first, second) + + def test_get_card_bogus_card(self): + response = views.get_card('__startCardasdfasdf') + expected = {"status":"failure","reason":"Card '__startCardasdfasdf' not found"} + self.assertEqual(response.status, '404') + + + def test_get_card_version(self): + __startCard_v1 = copy.deepcopy(self.__startCard) + __startCard_v1['version'] = 1 + self.assertEqual(__startCard_v1, views.get_card_version('__startCard', 1)) + + def test_get_card_version_twice(self): + first = views.get_card_version('__startCard', 1) + second = views.get_card_version('__startCard', 1) + self.assertEqual(first, second) + + def test_get_all_card_versions(self): + current = views.get_card_version('__startCard', 1) + previous = None + while current['next_version'] is not None: + previous = current + current = views.get_card_version('__startCard', int(current['next_version'])) + if current.status == 404: + break #we've run out of versions + if current is not None and previous is not None: + self.assertEqual(current['link'], previous['link']) + self.assertEqual(previous['version'] + 1, current['version']) + self.assertEqual(previous['next_version'], current['version']) + self.assertNotEqual(previous['content'], current['content']) + + def test_get_card_version_bogus_version(self): + response = views.get_card_version('__startCard', -1) + expected = {"status":"failure", "reason":"No version -1 found for card '__startCard'"} + self.assertEqual(404, response.status) + self.assertEqual(expected, response) + + def test_get_card_version_bogus_card(self): + response = views.get_card_version('bogon_from_space', 1) + expected = {"status":"failure", "reason":"No version 1 found for card 'bogon_from_space'"} + self.assertEqual(404, response.status) + self.assertEqual(expected, response) + + def test_get_card_version_bogus_card_and_bogus_version(self): + response = views.get_card_version('bogon_from_space', -1) + expected = {"status":"failure", "reason":"No version -1 found for card 'bogon_from_space'"} + self.assertEqual(404, response.status) + self.assertEqual(expected, response) + + def test_create_card(self): + views.request = MockCardRequest() + try: + created_card = views.create_card(views.request.json['link']) + expected = cardwiki.request_to_carddict(views.request) + self.assertEqual(expected['display_title'], created_card['display_title']) + self.assertEqual(expected['edited_by'], created_card['edited_by']) + self.assertEqual(expected['link'], created_card['link']) + self.assertEqual(expected['content'], created_card['content']) + self.assertEqual(expected['link'], created_card['link']) + self.assertEqual(expected['rendered_content'], created_card['rendered_content']) + self.assertIsNotNone(created_card['id']) + self.assertIsNotNone(created_card['version']) + self.assertIsNotNone(created_card['edited_at']) + finally: + with session_scope() as session: + cardwiki.delete_card(views.request.json['link'], session) + + def test_create_card_twice(self): + views.request = MockCardRequest() + try: + created_card = views.create_card(views.request.json['link']) + second_created_card = views.create_card(views.request.json['link']) + self.assertEquals(created_card, second_created_card) + finally: + with session_scope() as session: + cardwiki.delete_card(views.request.json['link'], session) + + def test_create_card_with_changes(self): + views.request = MockCardRequest() + try: + created_card = views.create_card(views.request.json['link']) + views.request.json['content'] = "totally different content" + created_card_with_changes = views.create_card(views.request.json['link']) + expected = cardwiki.request_to_carddict(views.request) + self.assertNotEqual(created_card, created_card_with_changes) + self.assertEqual(expected['display_title'], created_card_with_changes['display_title']) + self.assertEqual(expected['edited_by'], created_card_with_changes['edited_by']) + self.assertEqual(expected['link'], created_card_with_changes['link']) + self.assertEqual(expected['content'], created_card_with_changes['content']) + self.assertEqual(expected['link'], created_card_with_changes['link']) + self.assertEqual("

totally different content

", created_card_with_changes['rendered_content']) + self.assertIsNotNone(created_card_with_changes['id']) + self.assertIsNotNone(created_card_with_changes['version']) + self.assertIsNotNone(created_card_with_changes['edited_at']) + finally: + with session_scope() as session: + cardwiki.delete_card(views.request.json['link'], session) + + def test_create_card_with_same_changes_twice(self): + views.request = MockCardRequest() + try: + created_card = views.create_card(views.request.json['link']) + views.request.json['content'] = "totally different content" + created_card_with_changes = views.create_card(views.request.json['link']) + created_card_with_changes_second = views.create_card(views.request.json['link']) + self.assertNotEqual(created_card, created_card_with_changes_second) + self.assertEqual(created_card_with_changes, created_card_with_changes_second) + finally: + with session_scope() as session: + cardwiki.delete_card(views.request.json['link'], session) + + def test_create_card_request_does_not_match_uri(self): + views.request = MockCardRequest() + try: + expected = {"status":"failure", "reason":"resource uri does not match link in request"} + response = views.create_card('testcard') + self.assertEqual(400, response.status) + self.assertEqual(expected, response) + finally: + with session_scope() as session: + card = cardwiki.get_newest_card(views.request.json['link'], session) + if card is not None: + cardwiki.delete_card(views.request.json['link'], session) + + def test_delete_card(self): + views.request = MockCardRequest() + try: + created_card = views.create_card(views.request.json['link']) + response = views.delete_card(views.request.json['link']) + expected = {"status":"success", "deleted_card":views.request.json['link']} + self.assertEqual(expected, response) + + finally: + with session_scope() as session: + card = cardwiki.get_newest_card(views.request.json['link'], session) + if card is not None: + cardwiki.delete_card(views.request.json['link'], session) + + def test_delete_card_twice(self): + views.request = MockCardRequest() + try: + created_card = views.create_card(views.request.json['link']) + response = views.delete_card(views.request.json['link']) + response = viewsdelete_card(views.request.json['link']) + self.assertEqual(400, response.status) + finally: + with session_scope() as session: + card = cardwiki.get_newest_card(views.request.json['link'], session) + if card is not None: + cardwiki.delete_card(views.request.json['link'], session) + + def test_delete_card_nonexistent(self): + response = views.delete_card("bogon_from_space") + self.assertEqual(400, response.status) + + def test_get_card_tags(self): + response = views.get_card_tags('__startCard') + expected = {'tags':[{'tag':'administrivia','href':'/tags/administrivia'}]} + self.assertEqual(expected, response) + + def test_get_card_tags_twice(self): + first = views.get_card_tags('__startCard') + second = views.get_card_tags('__startCard') + self.assertEqual(first, second) + + def test_get_card_tags_bogus_card(self): + response = views.get_card_tags('bogon_from_space') + self.assertEqual(400, response.status) + + def test_create_card_tags(self): + views.request = MockTagRequest() + try: + response = views.create_card_tags('__startCard') + expected = {'tags': [{'href':'/tags/administrivia', + 'tag':'administrivia'}, + {'href': '/tags/descriptive_tag', + 'tag':'descriptive_tag'}]} + self.assertEqual(expected, response) + finally: + views.delete_card_tags('__startCard', 'descriptive_tag') + + def test_create_card_tags_multiple_tags(self): + views.request = MockTagRequest() + request.json['tags'].append({'tag':'another_tag', 'tagged_card':'__startCard'}) + try: + response = views.create_card_tags('__startCard') + expected = {'tags': [{'href':'/tags/administrivia', + 'tag':'administrivia'}, + {'href': '/tags/descriptive_tag', + 'tag':'descriptive_tag'}, + {'tag':'another_tag', + 'tagged_card':'__startCard'}]} + self.assertCountEqual(expected, response) + finally: + views.delete_card_tags('__startCard', 'descriptive_tag') + views.delete_card_tags('__startCard', 'another_tag') + + def test_create_card_tags_multiple_tags_twice(self): + views.request = MockTagRequest() + request.json['tags'].append({'tag':'another_tag', 'tagged_card':'__startCard'}) + try: + first = views.create_card_tags('__startCard') + second = views.create_card_tags('__startCard') + self.assertCountEqual(first, second) + finally: + views.delete_card_tags('__startCard', 'descriptive_tag') + views.delete_card_tags('__startCard', 'another_tag') + + def test_create_card_tags_twice(self): + views.request = MockTagRequest() + try: + views.create_card_tags('__startCard') + views.create_card_tags('__startCard') + response = views.create_card_tags('__startCard') + expected = {'tags': [{'href':'/tags/administrivia', + 'tag':'administrivia'}, + {'href': '/tags/descriptive_tag', + 'tag':'descriptive_tag'}]} + self.assertCountEqual(expected, response) + finally: + views.delete_card_tags('__startCard', 'descriptive_tag') + + def test_create_card_tags_wrong_card_uri(self): + views.request = MockTagRequest() + response = views.create_card_tags('bogon_from_space') + self.assertEqual(400, response.status) + + def test_create_card_tags_tag_does_not_match_uri(self): + #400 error when uri is valid, but request does not match uri, or is otherwise invalid + views.request = MockTagRequest() + views.request.json['tags'][0]['tagged_card'] = 'bogon_from_space' + expected = {'reason': "Card '__startCard' does not match {'tag':'descriptive_tag', 'tagged_card':'bogon_from_space'}", + 'status': 'failure'} + try: + views.create_card_tags('__startCard') + response = views.create_card_tags('__startCard') + self.assertEqual(400, response.status) + self.assertCountEqual(expected, response) + finally: + views.delete_card_tags('__startCard', 'descriptive_tag') + + def test_create_card_tags_tag_does_not_match_uri_and_uri_invalid(self): + #404 error when uri is invalid + views.request = MockTagRequest() + expected = {'reason': "Card 'bogon_from_space' does not exist", + 'status': 'failure'} + response = views.create_card_tags('bogon_from_space') + self.assertEqual(404, response.status) + self.assertEqual(expected, response) + + def test_create_card_tags_invalid_uri(self): + #404 error when uri is invalid + views.request = MockTagRequest() + view.request.json['tags'][0]['tagged_card'] = 'bogon_from_space' + expected = {'reason': "Card 'bogon_from_space' does not exist", + 'status': 'failure'} + response = views.create_card_tags('bogon_from_space') + self.assertEqual(404, response.status) + self.assertEqual(expected, response) + + def test_delete_card_tags(self): + views.request = MockTagRequest() + response = views.create_card_tags('__startCard') + views.delete_card_tags('__startCard', 'descriptive_tag') + expected = {'tags': [{'tag':'administrivia', 'href':'/tags/administrivia'}]} + self.assertEqual(expected, views.get_card_tags('__startCard')) + + def test_delete_card_tags_multiple(self): + views.request = MockTagRequest() + response = views.create_card_tags('__startCard') + views.delete_card_tags('__startCard', 'descriptive_tag') + views.delete_card_tags('__startCard', 'descriptive_tag') + views.delete_card_tags('__startCard', 'descriptive_tag') + views.delete_card_tags('__startCard', 'descriptive_tag') + expected = {'tags': [{'tag':'administrivia', 'href':'/tags/administrivia'}]} + self.assertEqual(expected, views.get_card_tags('__startCard')) + + def test_delete_card_tags_bogus_card_uri(self): + views.request = MockTagRequest() + response = views.create_card_tags('__startCard') + expected = {"status":"failure", "reason":"Tried deleting tag descriptive_tag for card bogon_from_space, but found no card"} + result = views.delete_card_tags('bogon_from_space', 'descriptive_tag') + self.assertEqual(404, response.status) + self.assertEqual(expected, result) + + def test_delete_card_tags_bogus_tag_bogus_card(self): + views.request = MockTagRequest() + response = views.create_card_tags('__startCard') + expected = {"status":"failure", "reason":"Tried deleting tag descriptive_tag for card bogon_from_space, but found no card"} + result = views.delete_card_tags('bogon_from_space', 'bogus_tag') + self.assertEqual(404, response.status) + self.assertEqual(expected, result) + +if __name__=='__main__': + unittest.main() + \ No newline at end of file diff --git a/cardwiki/views.py b/cardwiki/views.py index 5e1a86d..18cf3eb 100644 --- a/cardwiki/views.py +++ b/cardwiki/views.py @@ -1,5 +1,6 @@ -from bottle import get, post, put, delete, run, template, request, static_file -import cardwiki.db as db +from bottle import get, post, put, delete, run, template, request, static_file, request +import cardwiki +from cardwiki.db import session_scope from sqlalchemy import func, exists, distinct, and_ from sqlalchemy.orm.exc import NoResultFound import json @@ -11,98 +12,6 @@ base_url = "/" -def get_cards(session): - result = [] - max_v_by_title = session.query(db.Card, - func.max(db.Card.version).\ - label('max_version')).\ - group_by(db.Card.title).\ - subquery() - for card in session.query(max_v_by_title).\ - filter(and_(db.Card.version == max_v_by_title.c.max_version, - db.Card.title == max_v_by_title.c.title)).all(): - result.append({'title':card.title, - 'linkable_title':card.linkable_title, - 'current_version':card.max_version}) - return result - -def get_wikilinks(linkable_title, session): - card_exists = exists().where(db.Card.linkable_title == linkable_title) - if session.query(db.Card).filter(card_exists).count() > 0: - newest_card = session.query(db.Card).filter(db.Card.linkable_title == linkable_title).order_by(db.Card.version.desc()).first() - session.close() - wikilinks = [] - for wikilink in newest_card.wikilinks: - wikilink_dict = wikilink.to_dict(); - wikilink_dict['href'] = '{0}cards/{1}'.format(base_url, - wikilink.to_card.replace(" ", "_")) - wikilinks.append(wikilink_dict) - return wikilinks - else: - return [] - -def derive_title_link(title): - title = re.sub(r'[\ ]', '_', title) - title = re.sub(r'[^a-zA-Z0-9_~\-\.]', '', title) - return title - -def get_newest_card(linkable_title, session): - if session.query(db.Card).filter(db.Card.linkable_title==linkable_title).count() > 0: - return session.query(db.Card).filter(db.Card.linkable_title == linkable_title).order_by(db.Card.version.desc()).first() - else: - return None - -def insert_card(card, session): - session.add(card) - wikilinks = re.finditer('\[\[([\w0-9_ -]+)\]\]', request.json['content']) - for link in wikilinks: - wikilink = db.CardWikiLink(from_card = card.linkable_title, - to_card = link.group(0)[2:-2], - from_card_version = card.version) - session.add(wikilink) - return card - -def get_tags_for_card(linkable_title, session): - query = session.query(db.CardTag).filter(db.CardTag.tagged_card == linkable_title) - results = {"tags":[]} - for tag in query: - results["tags"].append({"tag":tag.tag, - "href":"{0}tags/{1}".format(base_url, - tag.tag)}) - return results - -def insert_tags(tags, session): - for tag in tags: - print(tag) - split_tags = re.split("[:,;+\.| ]", tag["tag"]) - print(split_tags) - for split_tag in split_tags: - if split_tag.strip() != "": - if session.query(db.CardTag).filter(db.CardTag.tagged_card == tag["tagged_card"], db.CardTag.tag == split_tag).count() == 0: - print("inserting tag") - session.add(db.CardTag(tagged_card = tag["tagged_card"], tag = split_tag)) - return tags - -def find_all_tags(session): - q = session.query(db.CardTag.tag, func.count(db.CardTag.tag)).group_by(db.CardTag.tag).all() - result = {"tags":[]} - for tag in q: - result["tags"].append({"tag":tag[0], "count":tag[1], "href":'{0}tags/{1}'.format(base_url, tag[0].replace(" ", "_"))}) - return result - -def find_cards_for_tag(tag, session): - tags = session.query(db.CardTag.tagged_card).filter(db.CardTag.tag == tag) - cards = session.query(db.Card).filter(db.Card.linkable_title.in_(tags)) - cards = cards.group_by(db.Card.linkable_title) - cards = cards.having(func.max(db.Card.version)) - result = {"cards":[]} - for card in cards: - card_dict = card.to_dict() - card_dict['href'] = '{0}cards/{1}'.format(base_url, card_dict['linkable_title']) - card_dict['wikilinks'] = '{0}cards/{1}/wikilinks'.format(base_url, card_dict['linkable_title']) - result["cards"].append(card_dict) - return result - def require_authentication(func): def check_auth(*args, **kwargs): with db.session_scope() as session: @@ -112,15 +21,15 @@ def check_auth(*args, **kwargs): try: password = request.json['password'] except: - return {"authentication_status":"failed", + return {"status":"failure", "requested_url":request.path, "reason":"We need both a username and password" } - return {"authentication_status":"failed", + return {"status":"failure", "requested_url":request.path(), "reason":"We need a username to go with your password" } - password = request.json['password'] + password = request.json['password'] user = session.query(db.User).filter(db.User.username == username).one() if bcrypt.verify(password, user.passwordhash): value = func(*args, **kwargs) @@ -128,33 +37,16 @@ def check_auth(*args, **kwargs): value['authentication_status']="success" print(value) return value - #return func(*args, **kwargs) + #return func(*args, **kwargs) else: - return {"authentication_status":"failure", + return {"status":"failure", "requested_url":request.path(), "reason":"We don't recognize your username with that password" } return check_auth -def perform_login(username, password, session): - try: - user = session.query(db.User).filter(db.User.username == username).one() - if bcrypt.verify(password, user.passwordhash): - user.last_seen = datetime.datetime.utcnow() - session.add(user) - return {'authentication_status':"success"} - else: - return {"authentication_status":"failure", - "requested_url":request.path(), - "reason":"We don't recognize your username with that password" - } - except NoResultFound: - return {"authentication_status":"failure", - 'reason':'User not found' - } - @post('{0}login/'.format(base_url)) -def login(): +def login(): missing_fields = [] try: username = request.json['username'] @@ -165,170 +57,97 @@ def login(): except: missing_fields.append("password") if len(missing_fields) > 0: - return {"authentication_status":"failure", + return {"status":"failure", 'missing_fields':missing_fields, 'reason':'Missing fields' } with db.session_scope() as session: - return perform_login(username, password, session) - + return cardwiki.perform_login(username, password, session) + @get('{0}'.format(base_url)) def get_index(): return static_file('index.html', root='.') - + @get('{0}static/'.format(base_url)) def get_static(filename): return static_file(filename, root='./static') - + @get('{0}cards/'.format(base_url)) def get_all_cards(): - with db.session_scope() as session: - return {"cards":get_cards(session)} - + with session_scope() as session: + return {"cards":cardwiki. + get_cards(session)} + @get('{0}cards/'.format(base_url)) -def get_card(linkable_title): - with db.session_scope() as session: - newest_card = get_newest_card(linkable_title, session) - if newest_card is None: - return {'linkable_title':linkable_title, - 'title':linkable_title.replace("_"," "), - 'content':None, - 'rendered_content':None, - 'edited_at':None, - 'edited_by':None, - 'version':0} - else: - return newest_card.to_dict() +def get_card(linkable_title): + with session_scope() as session: + result = cardwiki.get_newest_card(linkable_title, session) + if result is None or result == {}: + request.status = 404 + return {"status":"failure","reason":"Card {0} not found".format(linkable_title)} + return result -@get('{0}cards//wikilinks/'.format(base_url)) -def get_card_links(linkable_title): - with db.session_scope() as session: - return {'card': 'cards/{0}/'.format(linkable_title), - 'wikilinks': get_wikilinks(linkable_title, session)} - @get('{0}cards//'.format(base_url)) -def get_card(linkable_title,version): - '''redo this to use to_dict''' - with db.session_scope() as session: - newest_card = get_newest_card(linkable_title, session) - if newest_card is None: - return {'title':title, - 'content':None, - 'rendered_content':None, - 'edited_at':None, - 'edited_by':None, - 'version':0} - else: - return newest_card.to_dict() - +def get_card_version( linkable_title,version): + with session_scope() as session: + return cardwiki.get_card_version(linkable_title, version, session) + @put('{0}cards/'.format(base_url)) -@require_authentication +#@require_authentication def create_card(linkable_title): - with db.session_scope() as session: - newest_card = get_newest_card(linkable_title, session) - if newest_card is not None: - if newest_card.content != request.json['content']: - new_card = insert_card(db.Card(linkable_title=linkable_title, - title=request.json['title'].strip(), - version=newest_card.version + 1, - content=request.json['content'], - rendered_content = markdown.markdown(request.json['content'], extensions=[WikiLinkExtension(base_url='/cards/')]), - edited_at=datetime.datetime.utcnow(), - edited_by=None), - session) - else: - new_card = None - else: - new_card = insert_card(db.Card(linkable_title=linkable_title, - title=request.json['title'], - version=1, - content=request.json['content'], - rendered_content = markdown.markdown(request.json['content'], ['wikilinks(base_url={0}cards/)'.format(base_url)]), - edited_at=datetime.datetime.utcnow(), - edited_by=None), + card = cardwiki.request_to_carddict(request) + if linkable_title is not card['link']: + request.status = 400 + return {"status":"failure", "reason":"resource uri does not match link in request"} + with session_scope() as session: + return cardwiki.insert_card(card, session) - if new_card is None: - return newest_card.to_dict() - else: - return new_card.to_dict() - - +@delete('{0}cards/'.format(base_url)) +def delete_card(linkable_title): + with session_scope() as session: + try: + cardwiki.delete_card(linkable_title, session) + except cardwiki.CardNotFoundException as cardNotFound: + return {"status":"failure", "reason":cardNotFound.value} + return {"status":"success", "deleted_card":linkable_title} + @get('{0}cards//tags/'.format(base_url)) def get_card_tags(linkable_title): - with db.session_scope() as session: - return get_tags_for_card(linkable_title, session) + with session_scope() as session: + try: + return cardwiki.get_tags_for_card(linkable_title, session) + except cardwiki.CardNotFoundException as cardNotFound: + return {"status":"failure", "reason":cardNotFound.value} @put('{0}cards//tags/'.format(base_url)) -@require_authentication +#@require_authentication def create_card_tags(linkable_title): - ''' - session = db.session() - print(request.json['tags']) for tag in request.json['tags']: - print(tag) - split_tags = re.split("[:,;+\.| ]", tag["tag"]) - print(split_tags) - for split_tag in split_tags: - if split_tag.strip() != "": - if session.query(db.CardTag).filter(db.CardTag.tagged_card == tag["tagged_card"], db.CardTag.tag == split_tag).count() == 0: - print("inserting tag") - session.add(db.CardTag(tagged_card = tag["tagged_card"], tag = split_tag)) - session.commit() - session.close() - return get_card_tags(linkable_title) - ''' - with db.session_scope() as session: - insert_tags(request.json['tags'], session) - with db.session_scope() as session: - return get_tags_for_card(linkable_title, session) - - -''' -ACTUALLY DO THIS AT SOME POINT + if tag['tagged_card'] is not linkable_title: + print(tag['tag']) + return {"status":"failure", "reason":"Tag {{'tag': '{0}', 'tagged_card': '{1}'}} does not belong to card {2}".format(tag['tag'], tag['tagged_card'], linkable_title) + } + with session_scope() as session: + cardwiki.insert_tags(request.json['tags'], session) + with session_scope() as session: + return cardwiki.get_tags_for_card(linkable_title, session) + @delete('{0}cards//tags/'.format(base_url)) -@require_authentication -def create_card_tags(linkable_title, tag): - session = db.session() - db_tag = session.query(db.CardTag).filter(db.CardTag.tagged_card == linkable_title, db.CardTag.tag == tag).first() - session.delete(db_tag) - session.commit() - session.close() -''' - - - +#@require_authentication +def delete_card_tags(linkable_title, tag): + with session_scope() as session: + try: + cardwiki.delete_tag({"tag":tag, "tagged_card":linkable_title}, session) + except cardwiki.CardNotFoundException as cardNotFound: + return {"status":"failure", "reason":cardNotFound.value} + @get('{0}tags/'.format(base_url)) def get_all_tags(): - ''' - session = db.session() - q = session.query(db.CardTag.tag, func.count(db.CardTag.tag)).group_by(db.CardTag.tag).all() - session.close() - result = {"tags":[]} - for tag in q: - result["tags"].append({"tag":tag[0], "count":tag[1], "href":'{0}tags/{1}'.format(base_url, tag[0].replace(" ", "_"))}) - return result - ''' with db.session_scope() as session: - return find_all_tags(session) - + return cardwiki.find_all_tags(session) + @get('{0}tags/'.format(base_url)) def get_cards_for_tag(tag): - ''' - session = db.session() - tags = session.query(db.CardTag.tagged_card).filter(db.CardTag.tag == tag) - cards = session.query(db.Card).filter(db.Card.linkable_title.in_(tags)) - session.close() - cards = cards.group_by(db.Card.linkable_title) - cards = cards.having(func.max(db.Card.version)) - result = {"cards":[]} - print(cards) - for card in cards: - card_dict = card.to_dict() - card_dict['href'] = '{0}cards/{1}'.format(base_url, card_dict['linkable_title']) - card_dict['wikilinks'] = '{0}cards/{1}/wikilinks'.format(base_url, card_dict['linkable_title']) - result["cards"].append(card_dict) - return result - ''' with db.session_scope() as session: - find_cards_for_tag(tag, session) + return cardwiki.find_cards_for_tag(tag, session) diff --git a/cardwiki/wikilinks/__init__.py b/cardwiki/wikilinks/__init__.py index 9638d87..74b735c 100644 --- a/cardwiki/wikilinks/__init__.py +++ b/cardwiki/wikilinks/__init__.py @@ -1,18 +1,12 @@ ''' WikiLinks Extension for Python-Markdown ====================================== - Converts [[WikiLinks]] to relative links. - See for documentation. - Original code Copyright [Waylan Limberg](http://achinghead.com/). - All changes Copyright The Python Markdown Project - License: [BSD](http://www.opensource.org/licenses/bsd-license.php) - ''' from __future__ import absolute_import @@ -143,4 +137,4 @@ def _getMeta(self): def makeExtension(*args, **kwargs) : - return WikiLinkExtension(*args, **kwargs) + return WikiLinkExtension(*args, **kwargs) \ No newline at end of file diff --git a/index.html b/index.html index db325ed..76f686d 100644 --- a/index.html +++ b/index.html @@ -24,233 +24,12 @@ - + + - + function openAndGotoStartCard(){ + + }; + + function createNewCard(){ + + }; + + function listAllCards(){ + + }; + + function openAdmin(){ + + }; + + -
- +
diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..3565d9c --- /dev/null +++ b/init_db.py @@ -0,0 +1,42 @@ +"""Initializes a sqlite database with just enough information to use cardwiki""" +from cardwiki import db +import os +import markdown +from markdown.extensions.wikilinks import WikiLinkExtension + +def create_db(): + """Creates the initial sqlite database""" + card_id = 1 + + if os.path.isfile(db.DB_PATH): + #database exists, do nothing + pass + else: + #create the database + db.BASE.metadata.create_all(db.ENGINE) + #put the starting card in the db + with db.session_scope() as session: + session.add(db.User(username="admin", plainpassword="admin")) + content = """Welcome to *Card Wiki* +======================== + +Modify this card, or add new cards to get started. +Check out our documentation at our websites + +Use [[wikilinks]] to make new cards""" + rendered_content = markdown.markdown(content, + extensions=[WikiLinkExtension(base_url='/cards/')]) + + card = db.Card(display_title="", + link="__startCard", + content=content, + rendered_content=rendered_content, + edited_by='admin') + session.add(card) + session.flush() + card_id = card.id + tag = db.CardTag(tagged_card=card_id, tag="administrivia") + session.add(tag) + +if __name__ == '__main__': + create_db() diff --git a/static/css/custom.css b/static/css/custom.css index 9f2ac8a..08806c0 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -13,4 +13,12 @@ div.card_title_holder { a.externalLink{ text-decoration: underline; +} + +span.card_title{ + display: block; +} + +span.cardTitleEditor{ + display:inline; } \ No newline at end of file diff --git a/static/css/test.css b/static/css/test.css new file mode 100644 index 0000000..a5d07be --- /dev/null +++ b/static/css/test.css @@ -0,0 +1,3 @@ +.card * { + display:block; +} \ No newline at end of file diff --git a/static/js/cardwiki.js b/static/js/cardwiki.js new file mode 100644 index 0000000..e36ab2f --- /dev/null +++ b/static/js/cardwiki.js @@ -0,0 +1,396 @@ +String.prototype.format = String.prototype.f = function() { + var s = this + var i = arguments.length; + + while (i--) { + s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arguments[i]); + } + return s; +}; + +$.fn.goTo = function() { + $('html, body').animate({ + scrollTop: $(this).offset().top - '70px' + }, 'fast'); + return this; // for chaining... +} + +$.deselectAll = function() { + if (window.getSelection) { + if (window.getSelection().empty) { // Chrome + window.getSelection().empty(); + } else if (window.getSelection().removeAllRanges) { // Firefox + window.getSelection().removeAllRanges(); + } + } else if (document.selection) { // IE? + document.selection.empty(); + } +} + +function CardWiki(){ + this.cards = [] + this.editors = [] + this.username = null; + this.password = null; + } + +CardWiki.prototype.getCard = function(currentCardId, link, callback){ + if(this.cards[link] != null){ + if(this.cards[link] === "loading"){ + var that = this; + setTimeout(function(){ + that.getCard(currentCardId, link, callback); + }, 1200); + }else if(this.cards[link] == "error"){ + //server communications, halt until user tries again + this.cards[link] = null; + if(callback){ + callback(); + } + }else{ + $('#card_'+ link).goTo(); + if(callback){ + callback(); + } + } + }else{ + var that = this; + this.cards[link] = "loading"; + console.log('/cards/'+link); + $.ajax({url:'/cards/' + link, + type:'GET', + dataType:'json', + success: function(data){ + console.log(data); + var card = new Card(data); + console.log(data); + that.cards[card.link] = card; + if(currentCardId) + $(currentCardId).after(card.getHtml()); + else + $("#cardList").html(card.getHtml()); + if(card.content == null){ + card.editMode(); + }else{ + card.viewMode(); + //card.loadTags(); + } + //$("#card_"+card.link).goTo(); + if(callback){ + callback(); + } + }, + error: function(data){ + console.log(data); + that.cards[link] = "error"; + if(data.status == 404){ + $(currentCardId + " > div.announcements").html("

Something has gone very wrong, I can't find that card at all!

"); + }else{ + $(currentCardId + " > div.announcements").html("

The server fell over, try again in a bit. Give it room to breathe!!

"); + } + setTimeout(function(){$(currentCardId + " > div.announcements").html("");},10000); + if(callback){ + callback(); + } + } + }); + } +} + +var a = function(){ +console.log(data.link); + var card = new Card(data); + + +}; + +CardWiki.prototype.editCard = function(link) { + //link = fullLink.substring(7,fullLink.length - 1); + this.cards[link].editMode(); + var editor = null; + if (this.editors[link] == null){ + editor = new EpicEditor({container:"editor_" + link, + basePath:'/static', + autogrow:{minHeight:225, maxHeight:800, scroll:true}, + theme: {base: 'https://cdnjs.cloudflare.com/ajax/libs/epiceditor/0.2.2/themes/editor/epic-light.css', + preview: 'https://cdnjs.cloudflare.com/ajax/libs/epiceditor/0.2.2/themes/editor/epic-light.css', + editor: 'https://cdnjs.cloudflare.com/ajax/libs/epiceditor/0.2.2/themes/editor/epic-light.css'}}); + this.editors[link] = editor; + }else{ + editor = this.editors[link]; + } + editor.load(); + if ( editor.getFiles('cardWiki_' + link) === undefined){ + editor.importFile('cardWiki_' + link, this.cards[link].content); + }else{ + if( Date.parse(this.cards[link].edited_at) > Date.parse(editor.getFiles('cardWiki_' + link).modified) ){ + editor.importFile('cardWiki_' + link,this.cards[link].content); + }else{ + editor.open('cardWiki_' + link); + } + } + $('#card_'+ link).goTo(); +} + +CardWiki.prototype.cancelEditCard = function(link){ + // var link = obj.id.substring(21); + delete this.editors[link]; + $("#editor_"+link).empty(); + this.cards[link].viewMode(); +} + +CardWiki.prototype.removeCard = function(link){ + //var title = obj.id.substring(11); + $("#card_"+link).remove(); + this.editors[link] = null; + this.cards[link] = null; + +} + +CardWiki.prototype.requireLogin = function(callback, arg){ + console.log("requiring login"); +} + +CardWiki.prototype.saveCard = function(link, callback){ + //link = fullLink.substring(7,fullLink.length - 1); + var editor = this.editors[link]; + editor.save(); + var content = editor.exportFile('cardWiki_'+link); + var newCard = new Card({title:null, + link:link, + content:content, + rendered_content:null, + edited_at:null, + edited_by:this.username, + tags:[], + version:null}); + if($("#titleEditorInput_"+link)[0]){ + newCard.title = $("#titleEditorInput_"+link)[0].value; + }else{ + //consider whether cards need to have titles at all... + newCard.title = link; + } + if(this.username!=null){ + newCard.username=this.username; + newCard.password=this.password; + newCard.edited_by=this.username; + }else{ + this.requireLogin(); + return; + } + //perhaps we should just post to /cards/ as we don't know whether this exists or not + var that = this; + $.ajax({url:"/cards/"+link, + type:'PUT', + contentType:'application/json', + dataType:'json', + data:JSON.stringify(newCard), + success: function(data){ + if(data.authentication_status == "success"){ + $("#announcements_"+link).hide(); + link = data.link + card = new Card(data); + that.cards[link] = card; + card.viewMode(); + if(that.editors[link]!=null){ + that.editors[link] = null; + } + } else { + that.requireLogin(that.saveCard,"/cards/" + link) + } + if(callback){ + callback(); + } + }, + error: function(data){ + console.log("problem saving card"); + console.log(data); + $("#announcements_"+link).html("

Something has gone wrong, try again in a bit

"); + $("#announcements_"+link).show(); + if(callback){ + callback(); + } + } + }); +} + +CardWiki.prototype.getTags = function(cardLink, callback){ + //var taggedCardTitle = this.linkable_title; + $.ajax({url:'/cards/' + cardLink + '/tags/', + type:'GET', + dataType:'json', + success: function(data){ + console.log(data); + $('#tagsBox_' + cardLink + ' > input').importTags(''); + if(data.tags == null || data.tags.length < 1){ + //do nothing + }else{ + console.log(data); + this.tags = data.tags; + var tagVals = ""; + for( var i = 0; i < this.tags.length; i++){ + console.log(this.tags[i]); + if(tagVals == ""){ + tagVals = this.tags[i].tag; + }else{ + tagVals = tagVals + "," + this.tags[i].tag; + } + } + console.log(tagVals); + $('#tagsBox_' + cardLink + ' > input').importTags(tagVals); + } + if(callback){ + callback(); + } + }, + error: function(data){ + if(callback){ + callback(); + } + } + + }); +}; + +function listAllCards(){ + //$("#cardList").hide(); + //$("#admin").hide(); + //$("#queryDisplay").show(); + $.ajax({url:'/cards', + type:'GET', + success: function(data){ + console.log(data); + var tagCard = "
"+ + "
" + + "" + + "

{2}

" + + + "
" + + "
" + + "
"+ + "
{1}
"+ + "
"; + //var cardList = "

Tag: "+tag+"

    " + var cardList = "
      "; + for(var i = 0; i < data.cards.length; i++){ + cardList += "
    • {2}
    • ".format(data.cards[i].linkable_title, data.cards[i].href, data.cards[i].title) + } + cardList += "
    "; + console.log(cardList); + tagCard = tagCard.format("__adminAllCards", cardList, "All Cards"); + console.log($("div#cardList div.card:first-child")[0]); + $("div#cardList div.card:first").before(tagCard); + //$("#queryDisplay").html(tagCard); + } + }); +} + + function Card(jsonData) { + this.title = jsonData.title; + this.link= jsonData.link + this.content = jsonData.content; + this.rendered_content = jsonData.rendered_content; + this.edited_at = jsonData.edited_at; + this.edited_by = jsonData.edited_by; + //takes a seperate post to get these + this.tags = []; + this.version = jsonData.version; + } + +Card.card_template = "
    "+ + "
    " + + "" + + "

    {2}

    " + + "" + + "
    " + + "
    " + + "
    "+ + "
    "+ + "
    {1}
    "+ + "
    " + + "
    " + + "

    "+ + "Edit"+ + "Save"+ + "Cancel"+ + "

    " + + "
    "+ + "
    "; + +Card.card_template_sans_title = "
    " + + "
    "+ + "
    " + + "
    " + + "
    "+ + "
    {1}
    "+ + "
    " + + "
    " + + "

    "+ + "Edit"+ + "Save"+ + "Cancel"+ + "

    "+ + "
    "; + +Card.prototype.getHtml = function(){ + if(this.link.slice(0,2) == "__"){ + return Card.card_template_sans_title.format(this.link, this.rendered_content); + }else{ + var pretty_title = this.title.replace("_", " "); + return Card.card_template.format(this.link, this.rendered_content, this.title); + } +}; +(function($) { + $.fn.querytagsinput = function(){ + console.log(delimiter); + } +}) + +Card.prototype.viewMode = function(){ + //card_'+this.link).show(); + $("div#content_"+ this.link).html(this.rendered_content); + $('a#saveButton_'+this.link).hide(); + $('a#cancelCardEditButton_'+this.link).hide(); + $('span#titleEditor_'+this.link).hide(); + $("span#title_"+ this.link).show(); + $("div#content_"+ this.link).show(); + $("a#editButton_"+ this.link).show(); + $("div#editor_"+this.link).hide(); + console.log($('#card_'+this.link)); + $("div#card_"+this.link).goTo(); + var taggedCardTitle = this.link; + if(!$('#tags_' + this.link + "_tagsinput").length){ + var that = this; + $('#tags_' + this.link).tagsInput({ + height:'100%', + width:'85%', + overwriteTagInput: false, + onRemoveTag:function(tag){that.deleteTag(tag);}, + onAddTag:function(tag){that.addTag( tag);}, + onClickTag:function(tag){that,tagClicked(taggedCardTitle);} + }); + } +}; + +Card.prototype.editMode = function(){ + $.deselectAll(); + $("#title_"+ this.link).hide(); + $("#titleEditor_"+ this.link).show(); + $("#content_"+ this.link).hide(); + $("#editButton_"+ this.link).hide(); + $("#saveButton_"+ this.link).show(); + $("#cancelCardEditButton_"+ this.link).show(); + $("#editor_"+this.link).show(); +}; + +Card.prototype.deleteTag = function(tag){ + console.log(tag); +}; + +Card.prototype.addTag = function(tag){ + console.log(tag); +}; + +Card.prototype.tagClicked = function(tag){ + console.log(tag); +} \ No newline at end of file diff --git a/static/js/jquery.tagsinput2.js b/static/js/jquery.tagsinput2.js new file mode 100644 index 0000000..344d1b5 --- /dev/null +++ b/static/js/jquery.tagsinput2.js @@ -0,0 +1,364 @@ +/* + jQuery Tags Input Plugin 1.3.3 + + Copyright (c) 2011 XOXCO, Inc + + Documentation for this plugin lives here: + http://xoxco.com/clickable/jquery-tags-input + + Licensed under the MIT license: + http://www.opensource.org/licenses/mit-license.php + ben@xoxco.com +*/ + +(function($) { + + var delimiter = new Array(); + var tags_callbacks = new Array(); + $.fn.doAutosize = function(o){ + var minWidth = $(this).data('minwidth'), + maxWidth = $(this).data('maxwidth'), + val = '', + input = $(this), + testSubject = $('#'+$(this).data('tester_id')); + + if (val === (val = input.val())) {return;} + + // Enter new content into testSubject + var escaped = val.replace(/&/g, '&').replace(/\s/g,' ').replace(//g, '>'); + testSubject.html(escaped); + // Calculate new width + whether to change + var testerWidth = testSubject.width(), + newWidth = (testerWidth + o.comfortZone) >= minWidth ? testerWidth + o.comfortZone : minWidth, + currentWidth = input.width(), + isValidWidthChange = (newWidth < currentWidth && newWidth >= minWidth) + || (newWidth > minWidth && newWidth < maxWidth); + + // Animate width + if (isValidWidthChange) { + input.width(newWidth); + } + + + }; + $.fn.resetAutosize = function(options){ + // alert(JSON.stringify(options)); + var minWidth = $(this).data('minwidth') || options.minInputWidth || $(this).width(), + maxWidth = $(this).data('maxwidth') || options.maxInputWidth || ($(this).closest('.tagsinput').width() - options.inputPadding), + val = '', + input = $(this), + testSubject = $('').css({ + position: 'absolute', + top: -9999, + left: -9999, + width: 'auto', + fontSize: input.css('fontSize'), + fontFamily: input.css('fontFamily'), + fontWeight: input.css('fontWeight'), + letterSpacing: input.css('letterSpacing'), + whiteSpace: 'nowrap' + }), + testerId = $(this).attr('id')+'_autosize_tester'; + if(! $('#'+testerId).length > 0){ + testSubject.attr('id', testerId); + testSubject.appendTo('body'); + } + + input.data('minwidth', minWidth); + input.data('maxwidth', maxWidth); + input.data('tester_id', testerId); + input.css('width', minWidth); + }; + + $.fn.addTag = function(value,options) { + options = jQuery.extend({focus:false,callback:true},options); + this.each(function() { + var id = $(this).attr('id'); + + var tagslist = $(this).val().split(delimiter[id]); + if (tagslist[0] == '') { + tagslist = new Array(); + } + + value = jQuery.trim(value); + + if (options.unique) { + var skipTag = $(this).tagExist(value); + if(skipTag == true) { + //Marks fake input as not_valid to let styling it + $('#'+id+'_tag').addClass('not_valid'); + } + } else { + var skipTag = false; + } + + if (value !='' && skipTag != true) { + $('').addClass('tag').append( + $('').text(value).append('  '), + $('', { + href : '#', + title : 'Removing tag', + text : 'x' + }).click(function () { + return $('#' + id).removeTag(escape(value)); + }) + ).insertBefore('#' + id + '_addTag'); + + tagslist.push(value); + + $('#'+id+'_tag').val(''); + if (options.focus) { + $('#'+id+'_tag').focus(); + } else { + $('#'+id+'_tag').blur(); + } + + $.fn.tagsInput.updateTagsField(this,tagslist); + + if (options.callback && tags_callbacks[id] && tags_callbacks[id]['onAddTag']) { + var f = tags_callbacks[id]['onAddTag']; + f.call(this, value); + } + if(tags_callbacks[id] && tags_callbacks[id]['onChange']) + { + var i = tagslist.length; + var f = tags_callbacks[id]['onChange']; + f.call(this, $(this), tagslist[i-1]); + } + } + + }); + + return false; + }; + + $.fn.removeTag = function(value) { + value = unescape(value); + this.each(function() { + var id = $(this).attr('id'); + + var old = $(this).val().split(delimiter[id]); + + $('#'+id+'_tagsinput .tag').remove(); + str = ''; + for (i=0; i< old.length; i++) { + if (old[i]!=value) { + str = str + delimiter[id] +old[i]; + } + } + + $.fn.tagsInput.importTags(this,str); + + if (tags_callbacks[id] && tags_callbacks[id]['onRemoveTag']) { + var f = tags_callbacks[id]['onRemoveTag']; + f.call(this, value); + } + }); + + return false; + }; + + $.fn.tagExist = function(val) { + var id = $(this).attr('id'); + var tagslist = $(this).val().split(delimiter[id]); + return (jQuery.inArray(val, tagslist) >= 0); //true when tag exists, false when not + }; + + // clear all existing tags and import new ones from a string + $.fn.importTags = function(str) { + id = $(this).attr('id'); + $('#'+id+'_tagsinput .tag').remove(); + $.fn.tagsInput.importTags(this,str); + } + + $.fn.unloadTagsInput = function(){ + + } + + $.fn.tagsInput = function(options) { + var settings = jQuery.extend({ + interactive:true, + defaultText:'add a tag', + minChars:0, + width:'300px', + height:'100px', + autocomplete: {selectFirst: false }, + 'hide':true, + 'delimiter':',', + 'unique':true, + removeWithBackspace:true, + placeholderColor:'#666666', + autosize: true, + comfortZone: 20, + inputPadding: 6*2, + overwriteTagInput: true + },options); + + this.each(function() { + if (settings.hide) { + $(this).hide(); + } + + var id = $(this).attr('id'); + if(settings.overwriteTagInput){ + if (!id || delimiter[$(this).attr('id')]) { + id = $(this).attr('id', 'tags' + new Date().getTime()).attr('id'); + } + }else{ + if (!id) { + id = $(this).attr('id', 'tags' + new Date().getTime()).attr('id'); + } + } + + var data = jQuery.extend({ + pid:id, + real_input: '#'+id, + holder: '#'+id+'_tagsinput', + input_wrapper: '#'+id+'_addTag', + fake_input: '#'+id+'_tag' + },settings); + + delimiter[id] = data.delimiter; + + if (settings.onAddTag || settings.onRemoveTag || settings.onChange || settings.onClickTag) { + tags_callbacks[id] = new Array(); + tags_callbacks[id]['onAddTag'] = settings.onAddTag; + tags_callbacks[id]['onRemoveTag'] = settings.onRemoveTag; + tags_callbacks[id]['onChange'] = settings.onChange; + tags_callbacks[id]['onClickTag'] = settings.onClickTag; + } + + var markup = '
    '; + + if (settings.interactive) { + markup = markup + ''; + } + + markup = markup + '
    '; + + $(markup).insertAfter(this); + + $(data.holder).css('width',settings.width); + $(data.holder).css('min-height',settings.height); + $(data.holder).css('height','100%'); + + if ($(data.real_input).val()!='') { + $.fn.tagsInput.importTags($(data.real_input),$(data.real_input).val()); + } + if (settings.interactive) { + $(data.fake_input).val($(data.fake_input).attr('data-default')); + $(data.fake_input).css('color',settings.placeholderColor); + $(data.fake_input).resetAutosize(settings); + + $(data.holder).bind('click',data,function(event) { + $(event.data.fake_input).focus(); + }); + + $(data.fake_input).bind('focus',data,function(event) { + if ($(event.data.fake_input).val()==$(event.data.fake_input).attr('data-default')) { + $(event.data.fake_input).val(''); + } + $(event.data.fake_input).css('color','#000000'); + }); + + if (settings.autocomplete_url != undefined) { + autocomplete_options = {source: settings.autocomplete_url}; + for (attrname in settings.autocomplete) { + autocomplete_options[attrname] = settings.autocomplete[attrname]; + } + + if (jQuery.Autocompleter !== undefined) { + $(data.fake_input).autocomplete(settings.autocomplete_url, settings.autocomplete); + $(data.fake_input).bind('result',data,function(event,data,formatted) { + if (data) { + $('#'+id).addTag(data[0] + "",{focus:true,unique:(settings.unique)}); + } + }); + } else if (jQuery.ui.autocomplete !== undefined) { + $(data.fake_input).autocomplete(autocomplete_options); + $(data.fake_input).bind('autocompleteselect',data,function(event,ui) { + $(event.data.real_input).addTag(ui.item.value,{focus:true,unique:(settings.unique)}); + return false; + }); + } + + + } else { + // if a user tabs out of the field, create a new tag + // this is only available if autocomplete is not used. + $(data.fake_input).bind('blur',data,function(event) { + var d = $(this).attr('data-default'); + if ($(event.data.fake_input).val()!='' && $(event.data.fake_input).val()!=d) { + if( (event.data.minChars <= $(event.data.fake_input).val().length) && (!event.data.maxChars || (event.data.maxChars >= $(event.data.fake_input).val().length)) ) + $(event.data.real_input).addTag($(event.data.fake_input).val(),{focus:true,unique:(settings.unique)}); + } else { + $(event.data.fake_input).val($(event.data.fake_input).attr('data-default')); + $(event.data.fake_input).css('color',settings.placeholderColor); + } + return false; + }); + + } + // if user types a comma, create a new tag + $(data.fake_input).bind('keypress',data,function(event) { + if (event.which==event.data.delimiter.charCodeAt(0) || event.which==13 ) { + event.preventDefault(); + if( (event.data.minChars <= $(event.data.fake_input).val().length) && (!event.data.maxChars || (event.data.maxChars >= $(event.data.fake_input).val().length)) ) + $(event.data.real_input).addTag($(event.data.fake_input).val(),{focus:true,unique:(settings.unique)}); + $(event.data.fake_input).resetAutosize(settings); + return false; + } else if (event.data.autosize) { + $(event.data.fake_input).doAutosize(settings); + + } + }); + //Delete last tag on backspace + data.removeWithBackspace && $(data.fake_input).bind('keydown', function(event) + { + if(event.keyCode == 8 && $(this).val() == '') + { + event.preventDefault(); + var last_tag = $(this).closest('.tagsinput').find('.tag:last').text(); + var id = $(this).attr('id').replace(/_tag$/, ''); + last_tag = last_tag.replace(/[\s]+x$/, ''); + $('#' + id).removeTag(escape(last_tag)); + $(this).trigger('focus'); + } + }); + $(data.fake_input).blur(); + + //Removes the not_valid class when user changes the value of the fake input + if(data.unique) { + $(data.fake_input).keydown(function(event){ + if(event.keyCode == 8 || String.fromCharCode(event.which).match(/\w+|[αινσϊΑΙΝΣΪρΡ,/]+/)) { + $(this).removeClass('not_valid'); + } + }); + } + } // if settings.interactive + }); + + return this; + + }; + + $.fn.tagsInput.updateTagsField = function(obj,tagslist) { + var id = $(obj).attr('id'); + $(obj).val(tagslist.join(delimiter[id])); + }; + + $.fn.tagsInput.importTags = function(obj,val) { + $(obj).val(''); + var id = $(obj).attr('id'); + var tags = val.split(delimiter[id]); + for (i=0; iThis is the start card
    wikilink

    ", + edited_at:null, + edited_by:"admin", + tags:[], + version:1}); + +recipeCard = new Card({title:"Recipe Card", + link:"recipe_card", + content:"This is a recipe", + rendered_content:"

    This is a recipe

    ", + edited_at:null, + edited_by:"chef", + tags:[], + version:7}); + +card400 = new Card({title:"Card 400", + link:"card_400", + content:"This card throws a 400 error", + rendered_content:"

    This card throws a 400 error

    ", + edited_at:null, + edited_by:"badmonkey", + tags:[], + version:1}); + +$.mockjax({ + url: "/cards/recipe_card", + type: "put", + dataType: "json", + response: function(settings){ + var input = JSON.parse(settings.data); + if(input.version == null){ + input.version = 1; + } + var now = new Date() + this.responseText= JSON.stringify({"title":input.title, + "link":input.link, + "content":input.content, + "rendered_content":marked(input.content), + "edited_at":now.toISOString(), + "edited_by":input.edited_by, + "version":input.version, + "authentication_status":"success" + }); + return; + } +}); + +$.mockjax({ + url: "/cards/recipe_card", + type: "get", + dataType: "json", + responseText: {"title":"Recipe Card", + "link":"recipe_card", + "content":"This is a recipe", + "rendered_content":"

    This is a recipe

    ", + "edited_at":null, + "edited_by":"chef", + "tags":[], + "version":7} + +}); + +$.mockjax({ + url: "/cards/__startCard", + type: "get", + dataType: "json", + responseText: {"title":null, + "link":"__startCard", + "content":"This is the start card", + "rendered_content":"

    This is the start card wikilink

    ", + "edited_at":null, + "edited_by":"admin", + "tags":[], + "version":1} + +}); + +$.mockjax({ + url: "/cards/recipe_card/tags/", + type:"get", + dataType:"json", + responseText: {"tags":[{"tag":"cooking", + "tagged_card":"recipe_card", + "href":"/tags/cooking"}, + {"tag":"how-to", + "tagged_card":"recipe_card", + "href":"/tags/cooking"}], + "status":"success" + } +}); + +$.mockjax({ + url: "/cards/card_404/tags/", + type:"get", + dataType:"json", + status:404, + responseText: {"status":"failure", + "reason":"No Card '404_card' found"} +}); + +$.mockjax({ + url: "/cards/brand_new_card", + type: "get", + dataType: "json", + responseText: {"title":"Brand New Card", + "link":"brand_new_card", + "content":null, + "rendered_content":null, + "edited_at":null, + "edited_by":"chef", + "tags":[], + "version":7} + +}); + +$.mockjax({ + url:"/cards/card_400", + type: "put", + datatype: "json", + response : function(settings){ + this.status=400; + this.responseText= JSON.stringify({"status":"failure", + "reason":"resource uri does not match link in request"}) + } +}); + +$.mockjax({ + url:"/cards/card_404", + type: "get", + datatype: "json", + status:404, + response: function(settings){ + this.responseText= JSON.stringify({"status":"failure", + "reason":"No Card '404_card' found"}) + } +}); + +$.mockjax({ + url:"/cards/card_500", + type: "get", + datatype: "json", + status:500, + response: function(settings){ + this.responseText= JSON.stringify({"status":"failure", + "reason":"Something went badly wrong"}) + } +}); + +$.mockjax({ + url:"/cards/slow_card", + type: "get", + datatype: "json", + responseTime: 750, + response: function(settings){ + var now = new Date() + this.responseText= JSON.stringify({"title":"Slow Card", + "link":"slow_card", + "content":"This card is pretty slow", + "rendered_content":"

    This card is pretty slow

    ", + "edited_at":now.toISOString(), + "edited_by":"ghostinthemachine", + "version":1, + "authentication_status":"success" + }); + } +}); + +QUnit.module("cardwiki ui tests", { + beforeEach: function(){ + $("body").prepend(""); + cw = new CardWiki(); + cw.cards[__startCard.link] = __startCard; + cw.cards[recipeCard.link] = recipeCard; + }, + afterEach: function(){ + $("#cardList").remove(); +}}); + +QUnit.test( "CardWiki init test", function(assert) { + assert.deepEqual(cw.cards, [], "empty card array"); + assert.deepEqual(cw.editors, [], "empty editor array"); + assert,ok(cw.password == null, "no password"); + assert.ok(cw.username == null, "no username"); +}); + +QUnit.test( "CardWiki edit __ Card", function(assert) { + $('#cardList').append(__startCard.getHtml()); + cw.editCard('__startCard'); + $("#cardList").show(); + assert.ok(cw.editors[__startCard.link] != null, "epic editor exists"); + assert.ok(!$("#title_"+ __startCard.link).is(":visible"), "title is not showing"); + assert.ok(!$("#titleEditor_"+ __startCard.link).is(":visible"), "title editor is not showing"); + assert.ok(!$("#content_"+ __startCard.link).is(":visible"), "content is not showing"); + assert.ok(!$("#editButton_"+ __startCard.link).is(":visible"), "edit button is not showing"); + assert.ok($("#saveButton_"+ __startCard.link).is(":visible"),"save button is showing"); + assert.ok($("#cancelCardEditButton_"+ __startCard.link).is(":visible"),"cancel edit button is showing"); + assert.ok($("#editor_"+__startCard.link).is(":visible"),"editor is showing"); +}); + +QUnit.test( "CardWiki edit Card", function(assert) { + $('#cardList').append(recipeCard.getHtml()); + cw.editCard(recipeCard.link); + + $("#cardList").show(); + assert.ok(cw.editors[recipeCard.link] != null, "epic editor exists"); + assert.ok(!$("#title_"+ recipeCard.link).is(":visible"), "title is not showing"); + assert.ok($("#titleEditor_"+ recipeCard.link).is(":visible"), "title editor is showing"); + assert.ok(!$("#content_"+ recipeCard.link).is(":visible"), "content is not showing"); + assert.ok(!$("#editButton_"+ recipeCard.link).is(":visible"), "edit button is not showing"); + assert.ok($("#saveButton_"+ recipeCard.link).is(":visible"),"save button is showing"); + assert.ok($("#cancelCardEditButton_"+ recipeCard.link).is(":visible"),"cancel edit button is showing"); + assert.ok($("#editor_"+recipeCard.link).is(":visible"),"editor is showing"); +}); + +QUnit.test( "CardWiki cancel edit Card", function(assert) { + $('#cardList').append(recipeCard.getHtml()); + cw.editCard(recipeCard.link); + + $("#cardList").show(); + assert.ok(cw.editors[recipeCard.link] != null, "epic editor exists"); + cw.cancelEditCard(recipeCard.link); + assert.ok(cw.editors[recipeCard.link] == null, "editor removed from editor pool"); + assert.ok($("div.card_title_holder > span#title_"+ recipeCard.link).is(":visible"), "title is showing"); + assert.ok(!$("#titleEditor_"+ recipeCard.link).is(":visible"), "title editor is not showing"); + assert.ok($("#content_"+ recipeCard.link).is(":visible"), "content is showing"); + assert.ok($("#editButton_"+ recipeCard.link).is(":visible"), "edit button is showing"); + assert.ok(!$("#saveButton_"+ recipeCard.link).is(":visible"),"save button is not showing"); + assert.ok(!$("#cancelCardEditButton_"+ recipeCard.link).is(":visible"),"cancel edit button is not showing"); + assert.ok(!$("#editor_"+recipeCard.link).is(":visible"),"editor is not showing"); + +}); + +QUnit.test( "CardWiki remove Card", function(assert) { + + $('#cardList').append(recipeCard.getHtml()); + cw.editCard(recipeCard.link); + $("#cardList").show(); + cw.removeCard(recipeCard.link); + assert.ok(cw.editors[recipeCard.link] == null, "no editor"); + assert.ok(cw.cards[recipeCard.link] == null, "no card"); + +}); + +QUnit.test( "CardWiki save card", function(assert) { + cw.username = "test"; + cw.password = "test"; + $('#cardList').append(recipeCard.getHtml()); + $("#cardList").show(); + cw.editCard(recipeCard.link); + cw.editors[recipeCard.link].importFile('cardWiki_'+recipeCard.link, "totally different content, man"); + var done = assert.async(); + cw.saveCard(recipeCard.link, function(){ + assert.equal(cw.cards[recipeCard.link].content, "totally different content, man", "content appears updated"); + assert.equal(cw.cards[recipeCard.link].rendered_content, marked("totally different content, man"), "content appears rendered"); + assert.equal(cw.editors[recipeCard.link], null, "editor removed from editor pool"); + assert.ok($("div.card_title_holder > span#title_"+ recipeCard.link).is(":visible"), "title is showing"); + assert.ok(!$("#titleEditor_"+ recipeCard.link).is(":visible"), "title editor is not showing"); + assert.ok($("#content_"+ recipeCard.link).is(":visible"), "content is showing"); + assert.ok($("#editButton_"+ recipeCard.link).is(":visible"), "edit button is showing"); + assert.ok(!$("#saveButton_"+ recipeCard.link).is(":visible"),"save button is not showing"); + assert.ok(!$("#cancelCardEditButton_"+ recipeCard.link).is(":visible"),"cancel edit button is not showing"); + assert.ok(!$("#editor_"+recipeCard.link).is(":visible"),"editor is not showing"); + done() + }); +}); + +QUnit.test("CardWiki save card 400 error", function(assert){ + cw.cards[card400.link] = card400; + cw.username = "test"; + cw.password = "test"; + $('#cardList').append(card400.getHtml()); + $("#cardList").show(); + cw.editCard(card400.link); + cw.editors[card400.link].importFile('cardWiki_'+card400.link, "totally different content, man"); + var done = assert.async(); + cw.saveCard(card400.link, function(){ + assert.ok($("#editor_"+card400.link).is(":visible"),"editor is showing"); + assert.equal("

    Something has gone wrong, try again in a bit

    ", $("#announcements_"+card400.link).html(), "expected announcement"); + done(); + }); +}); + +QUnit.test("CardWiki get card", function(assert){ + cw.cards[recipeCard.link] = null; + $('#cardList').append(__startCard.getHtml()); + $("#cardList").show(); + var done = assert.async(); + cw.getCard("#card___startCard", recipeCard.link, function(){ + assert.ok($("#card_"+recipeCard.link).length, "recipe card exists"); + assert.ok($("div.card_title_holder > span#title_"+ recipeCard.link).is(":visible"), "title is showing"); + assert.ok(!$("#titleEditor_"+ recipeCard.link).is(":visible"), "title editor is not showing"); + assert.ok($("#content_"+ recipeCard.link).is(":visible"), "content is showing"); + assert.ok($("#editButton_"+ recipeCard.link).is(":visible"), "edit button is showing"); + assert.ok(!$("#saveButton_"+ recipeCard.link).is(":visible"),"save button is not showing"); + assert.ok(!$("#cancelCardEditButton_"+ recipeCard.link).is(":visible"),"cancel edit button is not showing"); + assert.ok(!$("#editor_"+recipeCard.link).is(":visible"),"editor is not showing"); + done(); + }); +}); + +QUnit.test("CardWiki get card, first card", function(assert){ + cw.cards[recipeCard.link] = null; + cw.cards[__startCard.link] = null; + $("#cardList").show(); + var done = assert.async(); + cw.getCard(null, __startCard.link, function(){ + assert.ok($("#card_"+__startCard.link).length, "__start card exists"); + assert.ok(!$("div.card_title_holder > span#title_"+ recipeCard.link).is(":visible"), "title is not showing"); + assert.ok(!$("#titleEditor_"+ __startCard.link).is(":visible"), "title editor is not showing"); + assert.ok($("#content_"+ __startCard.link).is(":visible"), "content is showing"); + assert.ok($("#editButton_"+ __startCard.link).is(":visible"), "edit button is showing"); + assert.ok(!$("#saveButton_"+ __startCard.link).is(":visible"),"save button is not showing"); + assert.ok(!$("#cancelCardEditButton_"+ __startCard.link).is(":visible"),"cancel edit button is not showing"); + assert.ok(!$("#editor_"+__startCard.link).is(":visible"),"editor is not showing"); + done(); + }); +}); + +QUnit.test("CardWiki get new card", function(assert){ + cw.cards[recipeCard.link] = null; + $('#cardList').append(__startCard.getHtml()); + $("#cardList").show(); + var done = assert.async(); + var brandNewCardLink = "brand_new_card"; + cw.getCard("#card___startCard", brandNewCardLink, function(){ + assert.ok($("#card_"+brandNewCardLink).length, "recipe card exists"); + assert.notEqual(cw.cards[brandNewCardLink], null); + assert.ok(!$("div.card_title_holder > span#title_"+ brandNewCardLink).is(":visible"), "title is Not showing"); + assert.ok($("#titleEditor_"+ brandNewCardLink).is(":visible"), "title editor is showing"); + assert.ok(!$("#content_"+ brandNewCardLink).is(":visible"), "content is not showing"); + assert.ok(!$("#editButton_"+ brandNewCardLink).is(":visible"), "edit button is not showing"); + assert.ok($("#saveButton_"+ brandNewCardLink).is(":visible"),"save button is showing"); + assert.ok($("#cancelCardEditButton_"+ brandNewCardLink).is(":visible"),"cancel edit button is showing"); + assert.ok($("#editor_"+brandNewCardLink).is(":visible"),"editor is showing"); + done(); + }); +}); + +QUnit.test("CardWiki get card twice", function(assert){ + cw.cards[recipeCard.link] = null; + $('#cardList').append(__startCard.getHtml()); + $("#cardList").show(); + + var done = assert.async(); + console.log("getting recipe card for first time"); + cw.getCard("#card___startCard", recipeCard.link, function(){ + console.log("inside second recipe card get"); + console.log($(".jumbotron.card")); + assert.equal($(".jumbotron.card").length, 2, "1st get - only one recipe card exists"); + done(); + }); + var done2 = assert.async(); + console.log("getting start card for second time"); + cw.getCard("#card___startCard", recipeCard.link, function(){ + console.log("recipe card gotten for second time"); + console.log($(".jumbotron.card")); + assert.equal($(".jumbotron.card").length, 2, "2nd get - only one recipe card exists 2"); + done2(); + }); + +}); + +QUnit.test("CardWiki get card twice, politely sequentially", function(assert){ + cw.cards[recipeCard.link] = null; + $('#cardList').append(__startCard.getHtml()); + $("#cardList").show(); + + var done = assert.async(); + var done2 = assert.async(); + cw.getCard("#card___startCard", recipeCard.link, function(){ + assert.equal($(".jumbotron.card").length, 2, "1st get - only one recipe card exists"); + assert.equal(cw.cards[recipeCard.link].link, recipeCard.link, "recipe card is in the cached list, same title"); + assert.equal(cw.cards[recipeCard.link].content, recipeCard.content, "recipe card is in the cached list, same content"); + + cw.getCard("#card___startCard", recipeCard.link, function(){ + assert.equal($(".jumbotron.card").length, 2, "2nd get - only one recipe card exists 2"); + done2(); + }); + done(); + }); + + +}); + +QUnit.test("CardWiki get card 5 times 'impatient clicker'", function(assert){ + cw.cards[recipeCard.link] = null; + $('#cardList').append(__startCard.getHtml()); + $("#cardList").show(); + var asyncCounter = 0; + var done = assert.async(); + console.log("getting recipe card for first time"); + cw.getCard("#card___startCard", recipeCard.link, function(){ + console.log("inside second recipe card get"); + console.log($(".jumbotron.card")); + assert.equal($(".jumbotron.card").length, 2, "1st get - only one recipe card exists"); + done(); + }); + var done2 = assert.async(); + console.log("getting start card for second time"); + cw.getCard("#card___startCard", recipeCard.link, function(){ + console.log("recipe card gotten for second time"); + console.log($(".jumbotron.card")); + assert.equal($(".jumbotron.card").length, 2, "2nd get - only one recipe card exists"); + done2(); + }); + var done3 = assert.async(); + console.log("getting start card for second time"); + cw.getCard("#card___startCard", recipeCard.link, function(){ + console.log("recipe card gotten for second time"); + console.log($(".jumbotron.card")); + assert.equal($(".jumbotron.card").length, 2, "3rd get - only one recipe card exists"); + done3(); + }); + var done4 = assert.async(); + console.log("getting start card for second time"); + cw.getCard("#card___startCard", recipeCard.link, function(){ + console.log("recipe card gotten for second time"); + console.log($(".jumbotron.card")); + assert.equal($(".jumbotron.card").length, 2, "4th get - only one recipe card exists"); + done4(); + }); + var done5 = assert.async(); + console.log("getting start card for second time"); + cw.getCard("#card___startCard", recipeCard.link, function(){ + console.log("recipe card gotten for second time"); + console.log($(".jumbotron.card")); + assert.equal($(".jumbotron.card").length, 2, "5th - only one recipe card exists"); + done5(); + }); +}); + +QUnit.test("CardWiki get 404'd card", function(assert){ + cw.cards[recipeCard.link] = null; + $('#cardList').append(__startCard.getHtml()); + $("#cardList").show(); + var done = assert.async(); + var done2 = assert.async(); + var card404Link = "card_404"; + cw.getCard("#card___startCard", card404Link, function(){ + assert.equal($("#card_"+card404Link).length, 0, "404 card does not exist"); + assert.equal(cw.cards[card404Link], "error"); + assert.equal($("#card___startCard > div.announcements").html(), "

    Something has gone very wrong, I can't find that card at all!

    ", "error announcement displayed in parent card"); + + setTimeout(function(){ + assert.equal($("#card___startCard > div.announcements").html(), "", "announcements clear after about 10 seconds"); + done2(); + }, 11000); + done(); + }); +}); + +QUnit.test("CardWiki get 404'd card twice, sequentially", function(assert){ + cw.cards[recipeCard.link] = null; + $('#cardList').append(__startCard.getHtml()); + $("#cardList").show(); + var done = assert.async(); + var done2 = assert.async(); + var card404Link = "card_404"; + cw.getCard("#card___startCard", card404Link, function(){ + assert.equal($("#card_"+card404Link).length, 0, "404 card does not exist"); + assert.equal(cw.cards[card404Link], "error"); + assert.equal($("#card___startCard > div.announcements").html(), "

    Something has gone very wrong, I can't find that card at all!

    ", "error announcement displayed in parent card"); + + cw.getCard("#card___startCard", card404Link, function(){ + assert.equal($("#card_"+card404Link).length, 0, "404 card does not exist"); + assert.equal(cw.cards[card404Link], null); + assert.equal($("#card___startCard > div.announcements").html(), "

    Something has gone very wrong, I can't find that card at all!

    ", "error announcement displayed in parent card"); + done2(); + }); + done(); + }); +}); + +QUnit.test("CardWiki get 404'd card twice, simultaneously", function(assert){ + cw.cards[recipeCard.link] = null; + $('#cardList').append(__startCard.getHtml()); + $("#cardList").show(); + var done = assert.async(); + var done2 = assert.async(); + var card404Link = "card_404"; + cw.getCard("#card___startCard", card404Link, function(){ + assert.equal($("#card_"+card404Link).length, 0, "404 card does not exist"); + assert.equal(cw.cards[card404Link], "error"); + assert.equal($("#card___startCard > div.announcements").html(), "

    Something has gone very wrong, I can't find that card at all!

    ", "error announcement displayed in parent card"); + done(); + }); + cw.getCard("#card___startCard", card404Link, function(){ + assert.equal($("#card_"+card404Link).length, 0, "404 card does not exist"); + assert.equal(cw.cards[card404Link], null); + assert.equal($("#card___startCard > div.announcements").html(), "

    Something has gone very wrong, I can't find that card at all!

    ", "error announcement displayed in parent card"); + done2(); + }); +}); + +QUnit.test("CardWiki get 500 error'd card", function(assert){ + cw.cards[recipeCard.link] = null; + $('#cardList').append(__startCard.getHtml()); + $("#cardList").show(); + var done = assert.async(); + var done2 = assert.async(); + var card500Link = "card_500"; + cw.getCard("#card___startCard", card500Link, function(){ + assert.equal($("#card_"+card500Link).length, 0, "404 card does not exist"); + assert.equal(cw.cards[card500Link], "error"); + assert.equal($("#card___startCard > div.announcements").html(), "

    The server fell over, try again in a bit. Give it room to breathe!!

    ", "error announcement displayed in parent card"); + + setTimeout(function(){ + assert.equal($("#card___startCard > div.announcements").html(), "", "announcements clear after about 10 seconds"); + done2(); + }, 11000); + done(); + }); +}); + +QUnit.test("CardWiki load tags for recipe_card", function(assert){ + $('#cardList').append(__startCard.getHtml()); + $('#cardList').append(recipeCard.getHtml()); + recipeCard.viewMode(); + $("#cardList").show(); + var done = assert.async(); + console.log($("#cardList").html()); + cw.getTags(recipeCard.link, function(){ + assert.equal($('#tags_' + recipeCard.link +"_tagsinput").children().length, 4, "correct number of tags present"); + $('#tags_' + recipeCard.link +"_tagsinput").children().each(function(index){ + var expected = "" + if(index == 0){ + expected = "cooking  x" + } else if(index == 1){ + expected = "how-to  x" + }else if(index ==2){ + expected = "" + }else{ + //expected should be "" + } + assert.equal($(this).html(), expected, "comparing tag " + index); + }); + done(); + }); +}); + +QUnit.test("CardWiki load tags for recipe_card twice, simultaneously", function(assert){ + $('#cardList').append(__startCard.getHtml()); + $('#cardList').append(recipeCard.getHtml()); + recipeCard.viewMode(); + $("#cardList").show(); + var done = assert.async(); + cw.getTags(recipeCard.link, function(){ + assert.equal($('#tags_' + recipeCard.link +"_tagsinput").children().length, 4, "correct number of tags present"); + assert.equal($('#tagsBox_' + recipeCard.link).children().length, 2, "correct number of recipe tag inputs"); + + + $('#tags_' + recipeCard.link +"_tagsinput").children().each(function(index){ + var expected = "" + if(index == 0){ + expected = "cooking  x" + } else if(index == 1){ + expected = "how-to  x" + }else if(index ==2){ + expected = "" + }else{ + //expected should be "" + } + assert.equal($(this).html(), expected, "comparing tag " + index); + }); + done(); + }); + + var done2 = assert.async(); + cw.getTags(recipeCard.link, function(){ + assert.equal($('#tags_' + recipeCard.link +"_tagsinput").children().length, 4, "correct number of tags present"); + assert.equal($('#tagsBox_' + recipeCard.link).children().length, 2, "correct number of recipe tag inputs"); + $('#tags_' + recipeCard.link +"_tagsinput").children().each(function(index){ + var expected = "" + if(index == 0){ + expected = "cooking  x" + } else if(index == 1){ + expected = "how-to  x" + }else if(index ==2){ + expected = "" + }else{ + //expected should be "" + } + assert.equal($(this).html(), expected, "comparing tag " + index); + }); + done2(); + }); +}); + +QUnit.test("CardWiki load tags for non existent card", function(assert){ + $('#cardList').append(recipeCard.getHtml()); + recipeCard.viewMode(); + $("#cardList").show(); + var done = assert.async(); + console.log($("#cardList").html()); + cw.getTags("card_404", function(){ + assert.ok($('#tags_' + recipeCard.link +"_tagsinput > div#tags_" + recipeCard.link + "_addTag"), "add tag div is there"); + assert.ok($('#tags_' + recipeCard.link +"_tagsinput > div.tags_clear"), "clear tag div is there"); + assert.equal($('#tags_' + recipeCard.link +"_tagsinput").children().length, 2, "correct number of tags present"); + done(); + }); +}); + +QUnit.test("Card constructor", function(assert){ + var now = new Date() + var cardJson = { + title: "New Title", + link: "new_title", + content: "test content", + rendered_content: "

    test content

    ", + edited_at:now.toISOString(), + edited_by: "test user", + tags:[], + version:1 + }; + var testCard = new Card(cardJson); + assert.equal(testCard.title, cardJson.title, "titles match"); + assert.equal(testCard.link, cardJson.link, "links match"); + assert.equal(testCard.content, cardJson.content, "contents match"); + assert.equal(testCard.rendered_content, cardJson.rendered_content, "rendered contents match"); + assert.equal(testCard.edited_at, cardJson.edited_at, "edited_ats match"); + assert.equal(testCard.edited_by, cardJson.edited_by, "edited_bys match"); + assert.deepEqual(testCard.tags, [], "tags empty"); + assert.equal(testCard.version, cardJson.version, "versions match"); + + var cardJsonNoTags = { + title: "New Title", + link: "new_title", + content: "test content", + rendered_content: "

    test content

    ", + edited_at:now.toISOString(), + edited_by: "test user", + version:1 + }; + var testCard2 = new Card(cardJsonNoTags); + assert.equal(testCard2.title, cardJsonNoTags.title, "titles match"); + assert.equal(testCard2.link, cardJsonNoTags.link, "links match"); + assert.equal(testCard2.content, cardJsonNoTags.content, "contents match"); + assert.equal(testCard2.rendered_content, cardJsonNoTags.rendered_content, "rendered contents match"); + assert.equal(testCard2.edited_at, cardJsonNoTags.edited_at, "edited_ats match"); + assert.equal(testCard2.edited_by, cardJsonNoTags.edited_by, "edited_bys match"); + assert.deepEqual(testCard2.tags, [], "tags empty"); + assert.equal(testCard2.version, cardJsonNoTags.version, "versions match"); + + var cardJsonTags = { + title: "New Title", + link: "new_title", + content: "test content", + rendered_content: "

    test content

    ", + edited_at:now.toISOString(), + edited_by: "test user", + tags: [{"tag":"taggo",},{"tag":"poop"}], + version:1 + }; + var testCard3 = new Card(cardJsonTags); + assert.equal(testCard3.title, cardJsonTags.title, "titles match"); + assert.equal(testCard3.link, cardJsonTags.link, "links match"); + assert.equal(testCard3.content, cardJsonTags.content, "contents match"); + assert.equal(testCard3.rendered_content, cardJsonTags.rendered_content, "rendered contents match"); + assert.equal(testCard3.edited_at, cardJsonTags.edited_at, "edited_ats match"); + assert.equal(testCard3.edited_by, cardJsonTags.edited_by, "edited_bys match"); + assert.deepEqual(testCard3.tags, [], "tags empty"); + assert.equal(testCard3.version, cardJsonTags.version, "versions match"); +}); + diff --git a/test.html b/test.html new file mode 100644 index 0000000..db4740a --- /dev/null +++ b/test.html @@ -0,0 +1,23 @@ + + + + + CardWiki UI Testing + + + + + + + + + + + + + +
    +
    + + + \ No newline at end of file