# HG changeset patch # User Filip de Waard # Date 1272576196 -7200 # Node ID 64923cbb45345a6a3ef0af23c76e6078bf348ec7 # Parent 0fd66564ac9275e8f2043f6a9b09772088560cfe working on model.Feed diff -r 0fd66564ac9275e8f2043f6a9b09772088560cfe -r 64923cbb45345a6a3ef0af23c76e6078bf348ec7 .swp Binary file .swp has changed diff -r 0fd66564ac9275e8f2043f6a9b09772088560cfe -r 64923cbb45345a6a3ef0af23c76e6078bf348ec7 setup.py --- a/setup.py Fri Apr 23 03:51:29 2010 +0200 +++ b/setup.py Thu Apr 29 23:23:16 2010 +0200 @@ -17,7 +17,8 @@ "Pylons>=1.0rc1", "CouchDB", "bcrypt", - "testfixtures" + "testfixtures", + "pytz" ], dependency_links = [ "http://pylonshq.com/download/1.0rc1" diff -r 0fd66564ac9275e8f2043f6a9b09772088560cfe -r 64923cbb45345a6a3ef0af23c76e6078bf348ec7 vix/lib/util.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/vix/lib/util.py Thu Apr 29 23:23:16 2010 +0200 @@ -0,0 +1,347 @@ +# -*- coding: utf-8 -*- + +""" +vix/lib/util.py: Vix utility functions + +Copyright 2009-2010, Net Collective. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import re +from datetime import datetime, timedelta + +import pytz +import pytz.tzinfo + +_FILENAME_EXT_PATTERN = re.compile(r"(\.[a-zA-Z0-9\.]{1,15})$") +_URI_UNFRIENDLY_CHAR_PATTERN = re.compile(r"[\W_]+") +_ENDS_WITH_NUMBER_PATTERN = re.compile(r"-(\d+)$") +_TZ_PATTERN = re.compile(r"(.+)(\+|-)(\d{2}):(\d{2})$") +_ISO_8601_PATTERN = re.compile( + r"^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.?(\d{0,6})$") + +_XML_ESCAPE_MAP = [('&', '&'), ('<', '<'), ('>', '>'), + ('"', '"')] +_XML_UNESCAPE_MAP = [('<', '<'), ('>', '>'), ('"', '"'), + ('&', '&')] + +class TagURI(object): + """TagURI (:rfc:`4151`) UUID for a document. + + >>> today = datetime.datetime.today() + >>> TagURI("w3c.org", today, "/index.html") + u"tag:w3c.org,2009-02-28:/index.html" + >>> TagURI("w3c.org", today, "index.html", "technologies") + u"tag:w3c.org,2009-02-28:index.html#technologies" + + """ + + def __init__(self, authority_name, date, specific, fragment=None): + """ + :arg authority_name: + DNS name (like example.org) or e-mail address (like + user@example.org). The authority name must be assigned to + the entity minting the tag at 00:00 UTC on the given date. + :type authority_name: Unicode string + :arg date: + date on which the tag is minted (e.g. datetime.date.today() + for a new TagURI). + :type date: + datetime.date object + :arg specific: + Local identifier for the resource (e.g. 'images/airplane.png'). + :type specific: Unicode string. + :arg fragment: + Fragment identifier, indicating the tag is referencing + that specific fragment of the resource. Can be compared + to #toc in the HTTP URI /documentation.html#toc where + 'toc' is a reference to a element that + identifies the table of contents within the document. + :type fragment: Unicode string + + """ + + self.authority_name = authority_name + self.date = date + self.specific = specific + self.fragment = fragment + + def __str__(self): + """Returns string representation of this TagURI.""" + + if self.fragment: + fragment = "#" + self.fragment + else: + fragment = "" + + return "tag:%s,%s:%s%s" % ( + self.authority_name, + "%04d-%02d-%02d" % ( + self.date.year, self.date.month, self.date.day), + self.specific, + fragment + ) + + def __unicode__(self): + return unicode(self.__str__()) + + def repr(self): + return self.__unicode__() + + +def split_filename(filename): + """Splits filename in basename and extension. + + If the provided filename doesn't have an extension + an empty unicode string is returned as the second + element in the returned tuple. Note that only + alphanumeric extensions that don't contain accented + characters are recognized. + + :arg filename: Possible filename to split. + :rtype: tuple (basename, extension) + + >>> split_filename(u"entries") + (u"entries", u"") + >>> split_filename(u"entries.xml") + (u"entries", u".xml") + >>> split_filename(u"compressed.tar.gz") + (u"compressed", u".tar.gz") + + """ + + parts = _FILENAME_EXT_PATTERN.split(filename) + + if len(parts) == 1: + return parts[0], u"" + elif len(parts) == 3: + return parts[0], parts[1] + +def normalize_local_identifier(string): + """ + Normalizes the namespace-specific part of a URI (e.g. TagURI or + HTTP URI) to human-friendly but valid formatting (see :rfc:`4151`). + + This namespace-specific part is used to locally identify the + resource, like a filename or normalized entry title. If the + provided string ends in what looks like a filename extension + this extension is stripped before normalization and appended + to the result. + + Converts the string to lowercase and replaces any sequence + of non-alpanumeric characters with dashes. Accented characters + are also removed (:rfc:`4151` recommends against including + percent-encoded characters in tags, in the interest of + tractability by humans). If the last character of the new + string is a dash that character is stripped. + + While the underscore ('_') character is perfectly valid in + both filenames and URI's this character is also replaced by + a dash ('-') to enforce a consistent style of separator use. + + >>> normalize_local_identifier(u'Quoth the Raven, "Nevermore."') + u"quoth-the-raven-nevermore" + >>> normalize_local_identifier(u"%#$@! my foot!") + u"my-foot" + >>> normalize_local_identifier(u"image_3.jpeg") + u"image-3.jpeg" + + :arg string: namespace-specific part of URI to normalize. + :returns: normalized namespace-specific part of URI. + :rtype: unicode + + """ + + basename, extension = split_filename(string.lower()) + basename = u"-".join(_URI_UNFRIENDLY_CHAR_PATTERN.split(basename)) + + if basename.endswith("-"): + return basename[:-1] + extension + else: + return basename + extension + + +def increment_id(string): + """Increments identifier to enforce uniqueness in a filename or URI. + + If the string ends with what looks like a filename extension + this extension is stripped before incrementing and appended + to the result. + + >>> increment_id(u"rambo") + u"rambo-2" + >>> increment_id(u"rambo-63") + u"rambo-64" + >>> increment_id(u"rambo-4.html") + u"rambo-5.html" + + :arg string: identifier to increment. + :returns: incremented identifier that is converted to lowercase. + + """ + + basename, extension = split_filename(string.lower()) + match = _ENDS_WITH_NUMBER_PATTERN.search(basename) + + if match: + incr_basename = basename[:match.start()] + u"-" + unicode( + int(match.group(1)) + 1) + else: + incr_basename = basename + u"-2" + + return incr_basename + extension + + +def datetime_to_rfc3339(dt, timezone=None): + """Converts datetime object to rfc3339 string. + + A timezone object should be provided when the + datetime object isn't offset-aware. If an offset-naive + datetime object represents a UTC time the timezone can + be left out, as UTC is used by default. When the datetime + object isn't offset-naive its attached timezone will be used + instead of the timezone argument (making it redundant). + + Usage: + + >>> now = datetime(2009, 3, 7, 17, 43, 47) + >>> datetime_to_rfc3339(now) + u'2009-03-07T16:43:47Z' + >>> datetime_to_rfc3339(now, pytz.timezone("Europe/Amsterdam")) + u'2009-03-07T17:43:47+01:00' + + :arg dt: datetime object to convert + :arg timezone: + timezone that should be used for timezone naive + datetime objects (i.e. datetime objects that don't + have an attached tzinfo object) that represent a + time in a local timezone. Defaults to UTC. + :type timezone: pytz timezone + + :rtype: unicode + + """ + + if dt.tzinfo is None: + if not timezone: + timezone = pytz.utc + + dt = timezone.localize(dt) + + return unicode(dt.isoformat().replace(u"+00:00", u"Z")) + + +def rfc3339_to_datetime(string): + """Converts rfc3339 string to a datetime object. + + The returned datetime object is offset-aware, + meaning that it has a timezone object attached. + When an offset-naive datetime object is required + (for example when interacting with SQLAlchemy) + the timezone can be removed like this:: + + my_dt = rfc3339_to_datetime(u"2009-03-07T17:43:47Z") + my_dt = my_dt.replace(tzinfo=None) + + The timezone of the returned datetime object + represents the local timezone used in the provided + string (no conversion occurs). This function will raise + a ValueError when the provided string doesn't match :rfc:`3339`. + + Usage: + + >>> rfc3339_to_datetime(u"2009-03-07T17:43:47Z") + datetime(2009, 3, 7, 17, 43, 47, tzinfo=) + >>> rfc3339_to_datetime(u"2009-03-07T17:43:47+01:00") + datetime(2009, 3, 7, 17, 43, 47, tzinfo=) + + :arg string: string containing a :rfc:`3339` formatted date + :type string: unicode + + :rtype: datetime + + """ + + #remove leading/trailing whitespace + string = string.strip() + + #deal with the timezone + if string.endswith(u"Z"): + #Zulu indicates UTC + tz = pytz.utc + dt_str = string[:-1] + else: + tz_match = _TZ_PATTERN.match(string) + + if not tz_match: + #raise exception if we can't parse the timezone + raise ValueError(u"RFC 3339 dates must have a " + + u"valid UTC offset at the end.") + + dt_str, operator, offset_hours, offset_minutes = tz_match.groups() + offset_hours = int(offset_hours) + offset_minutes = int(offset_minutes) + + if operator == u"-": + offset_hours = 0 - offset_hours + offset_minutes = 0 - offset_minutes + + if offset_hours == 0 and offset_minutes == 0: + tz = pytz.utc + else: + tz = pytz.tzinfo.StaticTzInfo() + offset_minutes = offset_minutes + offset_hours * 60 + tz._utcoffset = timedelta(minutes=offset_minutes) + + #deal with the actual datetime + iso8601_match = _ISO_8601_PATTERN.match(dt_str) + + if not iso8601_match: + #raise exception if the provided string doesn't conform to ISO8601 + raise ValueError(u"The provided string doesn't seem to be a valid " + + "ISO 3339 date.") + + year = int(iso8601_match.group(1)) + month = int(iso8601_match.group(2)) + day = int(iso8601_match.group(3)) + hour = int(iso8601_match.group(4)) + minute = int(iso8601_match.group(5)) + second = int(iso8601_match.group(6)) + microsecond = iso8601_match.group(7) + + if len(microsecond) == 0: + microsecond = 0 + else: + microsecond = int(microsecond) + + return tz.localize( + datetime(year, month, day, hour, minute, second, microsecond)) + +def xml_escape(string): + """Replaces XML entities in string with safe equivalents.""" + + for token, replacement in _XML_ESCAPE_MAP: + if token in string: + string = string.replace(token, replacement) + + return string + +def xml_unescape(string): + """Replaces safe equivalents with their XML entities.""" + + for token, replacement in _XML_UNESCAPE_MAP: + if token in string: + string = string.replace(token, replacement) + + return string diff -r 0fd66564ac9275e8f2043f6a9b09772088560cfe -r 64923cbb45345a6a3ef0af23c76e6078bf348ec7 vix/model/__init__.py --- a/vix/model/__init__.py Fri Apr 23 03:51:29 2010 +0200 +++ b/vix/model/__init__.py Thu Apr 29 23:23:16 2010 +0200 @@ -29,6 +29,8 @@ from couchdb import schema +import vix.lib.util as util + _password_re = re.compile('\$2a\$[\d]{2}\$[A-z\d./]{53}') _username_re = re.compile('^[.\w]{2,30}$') @@ -49,16 +51,12 @@ that CouchDB is a schema-less database). :param username: unique identifier for the user. - :type username: unicode :param password: Bcrypt digest of the password - :type password: unicode :param mail: e-mail address. - :type mail: unicode :param real_name: the real name of the user. - :type real_name: unicode - :param created: creation date. + :param created: creation date (UTC). :type created: datetime - :param updated: date of last update. + :param updated: date of last update (UTC). :type updated: datetime """ @@ -108,9 +106,7 @@ 'actions': {'GET': True, 'POST': True, 'PUT': True, 'DELETE': True}}] :param database: CouchDB database name. - :type database: unicode :param feed: Name of the feed you need permissions for. - :type feed: unicode :return: List with the index of the requested permission as the first value and a dictionary with the actual permissions as the second value. @@ -133,9 +129,7 @@ ... admin=True, actions=perms} :param database: CouchDB database name. - :type database: unicode :param feed: Name of the feed where the permissions apply. - :type feed: unicode :param admin: Determines is the user can modify documents posted by others (default is False, meaning the user can only edit his own documents). @@ -179,9 +173,7 @@ to an existing feed. :param database: database name. - :type database: unicode :param feed: feed name. - :type feed: unicode """ @@ -211,12 +203,10 @@ token they can identify themselves for the duration of the session. :param id: authentication token (MD5 hash of random value). - :type id: unicode :param username: user asssociated with the session. - :type username: unicode - :param created: date and time when the Session was created. + :param created: date and time when the Session was created (UTC). :type created: datetime - :param expires: date and time the Session will expire. + :param expires: date and time the Session will expire (UTC). :type expires: datetime """ @@ -247,3 +237,66 @@ expires_stamp = mktime(self.created.timetuple()) + duration self.expires = datetime.fromtimestamp(expires_stamp) + + +class Feed(schema.Document): + """ + A Feed object describes a collection of documents. The naming is + derives from Atom feeds. + + :param id: `Tag URI `_ based resource ID. + :param title: Feed title. + :param subtitle: Feed subtitle. + :param published: DateTime stamp of the publish date of the Feed (UTC). + :type published: datetime + :param updated: DateTime stamp of the last update of either the Feed + or one of its documents (UTC). + :type updated: datetime + + """ + + id = schema.TextField() + type = schema.TextField(default=u'feed') + title = schema.TextField() + slug = schema.TextField() + subtitle = schema.TextField() + published = schema.DateTimeField() + updated = schema.DateTimeField() + + def __init__(self, authority, slug_suggestion=None, **values): + """ + Creates the feed using the slug_suggestion value for the Atom ID. + If the particular ID is already in use the slug is incremented. + + :arg authority: TagURI authority for the active site (e.g. vix.io) + :arg slug_suggestion: Desired identifier for the resource. + + """ + + super(Feed, self).__init__(**values) + + self.published = datetime.utcnow() + self.slug = u'/feeds/%s' % (slug_suggestion or self.title) + + by_slug = """function(doc) { + if(doc.slug) { + emit(doc.slug, doc) + } + }""" + + #prevent duplicate ID's + while len(db.query(by_slug, key=self.slug)) > 0: + self.slug = util.increment_id(self.slug) + + self.id = unicode(util.TagURI( + authority_name=authority, + date=self.published, + specific=self.slug)) + + def store(self, db): + """Stores the document with a fresh updated value.""" + + #refresh self.updated stamp + self.updated = datetime.utcnow() + + return super(Feed, self).store(db) diff -r 0fd66564ac9275e8f2043f6a9b09772088560cfe -r 64923cbb45345a6a3ef0af23c76e6078bf348ec7 vix/model/views.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/vix/model/views.py Thu Apr 29 23:23:16 2010 +0200 @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +""" +vix/tests/test_views.py: Tests for CouchDB view code. + +Copyright 2009-2010, Net Collective. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +""" + +from couchdb.design import ViewDefinition + +by_slug = ViewDefinition('/_design/by_slug', 'by_slug', + '''function(doc) { + if(doc.slug) { + emit(doc.slug, doc) + } + }''') diff -r 0fd66564ac9275e8f2043f6a9b09772088560cfe -r 64923cbb45345a6a3ef0af23c76e6078bf348ec7 vix/tests/test_models.py --- a/vix/tests/test_models.py Fri Apr 23 03:51:29 2010 +0200 +++ b/vix/tests/test_models.py Thu Apr 29 23:23:16 2010 +0200 @@ -22,8 +22,7 @@ import hashlib import random import time - -from datetime import datetime +import datetime import couchdb import bcrypt @@ -32,6 +31,25 @@ import vix.tests import vix.model as model +import vix.lib.util as util + + +class _datetime(object): + """Mock datetime object.""" + + def __init__(self, dt): + """ + :args dt: datetime object that should be returned by utcnow() + + """ + + self.dt = dt + + def utcnow(self): + return self.dt + + def fromtimestamp(self, stamp): + return datetime.datetime.fromtimestamp(stamp) class TestModel(vix.tests.DatabasePoweredTestCase): @@ -64,31 +82,28 @@ self.assertEquals(user.password, u'$2a$10$zovtWSOSm0PTsiuovPOxC.uEJxsEzVf0AlswKvgT/jtxMqf44.Kpi') - class _datetime(object): - def utcnow(self): - return datetime(2010, 4, 19, 11, 12, 5) - - class _datetime2(object): - def utcnow(self): - return datetime(2010, 4, 23, 3, 19, 40) - with Replacer() as r: - r.replace('vix.model.datetime', _datetime()) + r.replace('vix.model.datetime', _datetime( + datetime.datetime(2010, 4, 19, 11, 12, 5))) user = model.User(username=u'fmw2', password=p, mail=u'fmw@vix.io', real_name=u'Filip de Waard') - self.assertEquals(user.created, datetime(2010, 4, 19, 11, 12, 5)) + self.assertEquals(user.created, + datetime.datetime(2010, 4, 19, 11, 12, 5)) self.assertEquals(user.updated, None) - r.replace('vix.model.datetime', _datetime2()) + r.replace('vix.model.datetime', _datetime( + datetime.datetime(2010, 4, 23, 3, 19, 40))) #reload user user.store(model.db) user = model.User.load(model.db, u'fmw2') - self.assertEquals(user.created, datetime(2010, 4, 19, 11, 12, 5)) - self.assertEquals(user.updated, datetime(2010, 4, 23, 3, 19, 40)) + self.assertEquals(user.created, + datetime.datetime(2010, 4, 19, 11, 12, 5)) + self.assertEquals(user.updated, + datetime.datetime(2010, 4, 23, 3, 19, 40)) #TODO: add profile @@ -249,16 +264,10 @@ def _time(): return 1271706172.864481 - - class _datetime(object): - def utcnow(self): - return datetime(2010, 4, 19, 11, 12, 5) - - def fromtimestamp(self, stamp): - return datetime.fromtimestamp(stamp) with Replacer() as r: - r.replace('vix.model.datetime', _datetime()) + r.replace('vix.model.datetime', _datetime( + datetime.datetime(2010, 4, 19, 11, 12, 5))) r.replace('vix.model.time', _time) r.replace('vix.model.random.randint', _random) @@ -267,14 +276,14 @@ self.assertEquals(session.id, token) self.assertEquals(session.created, - datetime(2010, 4, 19, 11, 12, 5)) + datetime.datetime(2010, 4, 19, 11, 12, 5)) self.assertEquals(session.expires, - datetime(2010, 4, 19, 11, 13, 5)) + datetime.datetime(2010, 4, 19, 11, 13, 5)) #check with different duration session = model.Session(duration=300) self.assertEquals(session.expires, - datetime(2010, 4, 19, 11, 17, 5)) + datetime.datetime(2010, 4, 19, 11, 17, 5)) #make sure that the dates don't change when updating session.store(model.db) @@ -284,6 +293,56 @@ session.store(model.db) self.assertEquals(session.created, - datetime(2010, 4, 19, 11, 12, 5)) + datetime.datetime(2010, 4, 19, 11, 12, 5)) self.assertEquals(session.expires, - datetime(2010, 4, 19, 11, 17, 5)) + datetime.datetime(2010, 4, 19, 11, 17, 5)) + + def test_Feed(self): + """Test model.Feed object.""" + + with Replacer() as r: + r.replace('vix.model.datetime', _datetime( + datetime.datetime(2010, 4, 19, 11, 12, 5))) + + feed = model.Feed(title=u'Vix Weblog', + subtitle=u'Hic Sunt Dracones', + slug_suggestion=u'blog', + authority=u'vix.io') + + self.assertEquals(feed.title, u'Vix Weblog') + self.assertEquals(feed.subtitle, u'Hic Sunt Dracones') + + self.assertEquals(feed.published, datetime.datetime( + 2010, 4, 19, 11, 12, 5)) + self.assertEquals(feed.updated, None) + + feed.store(model.db) + + r.replace('vix.model.datetime', _datetime( + datetime.datetime(2010, 4, 29, 19, 5, 30))) + + feed.store(model.db) + self.assertEquals(feed.updated, datetime.datetime( + 2010, 4, 29, 19, 5, 30)) + + self.assertEquals(feed.slug, u'/feeds/blog') + self.assertEquals(feed.id, u'tag:vix.io,2010-04-19:/feeds/blog') + + #test if duplicate ID's are avoided correctly + f2 = model.Feed(title=u'Vix Weblog', + subtitle=u'Hic Sunt Dracones', + slug_suggestion=u'blog', + authority=u'vix.io') + f2.store(model.db) + + self.assertEquals(f2.slug, u'/feeds/blog-2') + self.assertEquals(f2.id, u'tag:vix.io,2010-04-29:/feeds/blog-2') + + f3 = model.Feed(title=u'Vix Weblog', + subtitle=u'Hic Sunt Dracones', + slug_suggestion=u'blog', + authority=u'vix.io') + f3.store(model.db) + + self.assertEquals(f3.slug, u'/feeds/blog-3') + self.assertEquals(f3.id, u'tag:vix.io,2010-04-29:/feeds/blog-3') diff -r 0fd66564ac9275e8f2043f6a9b09772088560cfe -r 64923cbb45345a6a3ef0af23c76e6078bf348ec7 vix/tests/test_util.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/vix/tests/test_util.py Thu Apr 29 23:23:16 2010 +0200 @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- + +""" +vix/tests/test_util.py: Tests for Vix utility functions + +Copyright 2009-2010, Net Collective. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +""" + +import unittest +from datetime import datetime, date + +import pytz + +from vix.lib import util + +class TestUtil(unittest.TestCase): + + def test_TagURI(self): + """Test if util.TagURI works properly.""" + + authority_name = u"vix.io" + today = date(2009, 2, 28) + + tagURI = util.TagURI(authority_name, today, + u"/entries/2009/02/28/the-gold-bug.html") + + self.assertEquals(tagURI.__unicode__(), + u"tag:vix.io,2009-02-28:/entries/2009/02/28/" + + u"the-gold-bug.html") + + tagURI = util.TagURI(authority_name, today, + u"/entries/2009/02/28/ligeia.html", + u"verse") + + self.assertEquals(tagURI.__unicode__(), + u"tag:vix.io,2009-02-28:/entries/2009/02/28/" + + u"ligeia.html#verse") + + #test TagURI without a date in the 'specific' + tagURI = util.TagURI(authority_name, today, u"/feeds/blog") + + self.assertEquals(tagURI.__unicode__(), + u"tag:vix.io,2009-02-28:/feeds/blog") + + + def test_split_filename(self): + """Test if filenames are split correctly.""" + + self.assertEquals(util.split_filename( + u"feed.atom"), + (u"feed", u".atom")) + + self.assertEquals(util.split_filename( + u"feed"), + (u"feed", u"")) + + self.assertEquals(util.split_filename( + u"archive.tar.gz"), + (u"archive", u".tar.gz")) + + def test_normalize_local_identifier(self): + """Test if identifiers are correctly normalized for use in URI's.""" + + self.assertEquals(util.normalize_local_identifier( + u"What do you want for Christmas?"), + u"what-do-you-want-for-christmas") + + self.assertEquals(util.normalize_local_identifier( + u"You $%#@! idiot!"), + u"you-idiot") + + self.assertEquals(util.normalize_local_identifier( + u"image_3.png"), + u"image-3.png") + + self.assertEquals(util.normalize_local_identifier( + u"The-Balloon-Hoax"), + u"the-balloon-hoax") + + self.assertEquals(util.normalize_local_identifier( + u"Boudewijn Büch"), + u"boudewijn-b-ch") + + def test_increment_id(self): + """Test if identifiers are incremented correctly.""" + + self.assertEquals(util.increment_id( + u"rambo"), + u"rambo-2") + + self.assertEquals(util.increment_id( + u"rambo-2"), + u"rambo-3") + + self.assertEquals(util.increment_id( + u"rambo-1023"), + u"rambo-1024") + + self.assertEquals(util.increment_id( + u"hello-2-world"), + u"hello-2-world-2") + + self.assertEquals(util.increment_id( + u"rambo.html"), + u"rambo-2.html") + + self.assertEquals(util.increment_id( + u"rambo-5.html"), + u"rambo-6.html") + + def test_datetime_to_rfc3339(self): + """Test util.datetime_to_rfc3339 conversion function.""" + + now = datetime(2009, 3, 7, 16, 43, 47) + amsterdam = pytz.timezone("Europe/Amsterdam") + us_eastern = pytz.timezone("US/Eastern") + + self.assertEquals(util.datetime_to_rfc3339(now), + u"2009-03-07T16:43:47Z") + self.assertEquals(util.datetime_to_rfc3339(now, amsterdam), + u"2009-03-07T16:43:47+01:00") + self.assertEquals(util.datetime_to_rfc3339(now, us_eastern), + u"2009-03-07T16:43:47-05:00") + + def test_rfc3339_to_datetime(self): + """Test util.rfc3339_to_datetime conversion function.""" + + #test if ValueError is raised it timezone is omitted + self.assertRaises(ValueError, util.rfc3339_to_datetime, + u"2009-03-07T17:43:47") + + #test if ValueError is raised if string doesn't match ISO8601 + self.assertRaises(ValueError, util.rfc3339_to_datetime, + u"2009/03/07 17:43:47+00:20") + + #test a date/time with a non-UTC timezone + tz = pytz.timezone("Europe/Amsterdam") + reference_dt = tz.localize( + datetime(2009, 03, 07, 17, 43, 47, 0)) + + self.assertEquals( + util.rfc3339_to_datetime(u"2009-03-07T17:43:47+01:00"), + reference_dt) + + #test a date/time with a Zulu timezone (UTC) + reference_dt = datetime(2009, 03, 07, 17, 43, 47, 0, + pytz.utc) + + self.assertEquals( + util.rfc3339_to_datetime(u"2009-03-07T17:43:47Z"), + reference_dt) + + #test a date/time with microseconds + reference_dt = datetime(2009, 03, 07, 17, 43, 47, 4, + pytz.utc) + + self.assertEquals( + util.rfc3339_to_datetime(u"2009-03-07T17:43:47.4Z"), + reference_dt) + + self.assertNotEquals( + util.rfc3339_to_datetime(u"2009-03-07T17:43:47.5Z"), + reference_dt) + + #test a date/time with leading and trailing whitespace + self.assertEquals( + util.rfc3339_to_datetime(u" 2009-03-07T17:43:47.4Z "), + reference_dt) + + def test_xml_escape(self): + """Test util.xml_escape function.""" + + self.assertEquals(util.xml_escape(u'>'), + u"<foo bar="foobar"/>&gt;") + + def test_xml_unescape(self): + """Test util.xml_unescape function.""" + + self.assertEquals( + util.xml_unescape(u"<foo bar="foobar"/>&gt;"), + u'>') diff -r 0fd66564ac9275e8f2043f6a9b09772088560cfe -r 64923cbb45345a6a3ef0af23c76e6078bf348ec7 vix/tests/test_views.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/vix/tests/test_views.py Thu Apr 29 23:23:16 2010 +0200 @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +""" +vix/tests/test_views.py: Tests for CouchDB view code. + +Copyright 2009-2010, Net Collective. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +""" + +import couchdb +import bcrypt + +from testfixtures import Replacer + +import vix.tests +import vix.model as model +import vix.model.views as views +import vix.lib.util as util + +class TestViews(vix.tests.DatabasePoweredTestCase): + + def test_by_slug(self): + """Test by_slug CouchDB view.""" + + views.by_slug.sync(model.db) + + #add some test data + for i in range(10): + feed = model.Feed(title=u'Vix Weblog', + subtitle=u'Hic Sunt Dracones', + slug_suggestion=u'blog', + authority=u'vix.io') + feed.store(model.db) + + blog_result = model.db.view('by_slug') + blog2_result = model.db.view('by_slug', key='blog') + + print repr(blog_result) + print repr(blog2_result) + assert False