aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorLibravatarAgustin Henze <tin@sluc.org.ar>2013-03-13 20:58:39 -0300
committerLibravatarAgustin Henze <tin@sluc.org.ar>2013-03-13 20:58:39 -0300
commit8b14a1e5b2ca574fdd4fd2377567ec98a110d4b6 (patch)
tree0895935489e4920d18824f7fb3a0d799649a27c3 /tests
parent878ba1152ebc64a4a2609d23c9e400a6111db642 (diff)
Imported Upstream version 5.4.2upstream/5.4.2
Diffstat (limited to 'tests')
-rw-r--r--tests/data/translated_titles/conf.py397
-rw-r--r--tests/data/translated_titles/stories/1.txt5
-rw-r--r--tests/data/translated_titles/stories/1.txt.es4
-rw-r--r--tests/test_command_import_wordpress.py142
-rw-r--r--tests/test_command_init.py15
-rw-r--r--tests/test_integration.py157
-rw-r--r--tests/test_rss_feeds.py12
-rw-r--r--tests/test_utils.py121
-rw-r--r--tests/wordpress_export_example.xml52
9 files changed, 779 insertions, 126 deletions
diff --git a/tests/data/translated_titles/conf.py b/tests/data/translated_titles/conf.py
new file mode 100644
index 0000000..69c7bc7
--- /dev/null
+++ b/tests/data/translated_titles/conf.py
@@ -0,0 +1,397 @@
+
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+import time
+
+##############################################
+# Configuration, please edit
+##############################################
+
+
+# Data about this site
+BLOG_AUTHOR = "Your Name"
+BLOG_TITLE = "Demo Site"
+# This is the main URL for your site. It will be used
+# in a prominent link
+SITE_URL = "http://nikola.ralsina.com.ar"
+# This is the URL where nikola's output will be deployed.
+# If not set, defaults to SITE_URL
+# BASE_URL = "http://nikola.ralsina.com.ar
+BLOG_EMAIL = "joe@demo.site"
+BLOG_DESCRIPTION = "This is a demo site for Nikola."
+
+# Nikola is multilingual!
+#
+# Currently supported languages are:
+# English -> en
+# Greek -> gr
+# German -> de
+# French -> fr
+# Polish -> pl
+# Russian -> ru
+# Spanish -> es
+# Italian -> it
+# Simplified Chinese -> zh-cn
+#
+# If you want to use Nikola with a non-supported language you have to provide
+# a module containing the necessary translations
+# (p.e. look at the modules at: ./nikola/data/themes/default/messages/fr.py).
+# If a specific post is not translated to a language, then the version
+# in the default language will be shown instead.
+
+# What is the default language?
+DEFAULT_LANG = "en"
+
+# What other languages do you have?
+# The format is {"translationcode" : "path/to/translation" }
+# the path will be used as a prefix for the generated pages location
+TRANSLATIONS = {
+ "en": "",
+ # Example for another language:
+ "es": "./es",
+}
+
+# Links for the sidebar / navigation bar.
+# You should provide a key-value pair for each used language.
+SIDEBAR_LINKS = {
+ DEFAULT_LANG: (
+ ('/archive.html', 'Archives'),
+ ('/categories/index.html', 'Tags'),
+ ),
+ "es": ()
+}
+
+
+##############################################
+# Below this point, everything is optional
+##############################################
+
+
+# post_pages contains (wildcard, destination, template, use_in_feed) tuples.
+#
+# The wildcard is used to generate a list of reSt source files
+# (whatever/thing.txt).
+# That fragment must have an associated metadata file (whatever/thing.meta),
+# and opcionally translated files (example for spanish, with code "es"):
+# whatever/thing.txt.es and whatever/thing.meta.es
+#
+# From those files, a set of HTML fragment files will be generated:
+# cache/whatever/thing.html (and maybe cache/whatever/thing.html.es)
+#
+# These files are combinated with the template to produce rendered
+# pages, which will be placed at
+# output / TRANSLATIONS[lang] / destination / pagename.html
+#
+# where "pagename" is specified in the metadata file.
+#
+# if use_in_feed is True, then those posts will be added to the site's
+# rss feeds.
+#
+
+post_pages = (
+ ("posts/*.txt", "posts", "post.tmpl", True),
+ ("stories/*.txt", "stories", "story.tmpl", False),
+)
+
+# One or more folders containing files to be copied as-is into the output.
+# The format is a dictionary of "source" "relative destination".
+# Default is:
+# FILES_FOLDERS = {'files': '' }
+# Which means copy 'files' into 'output'
+
+# A mapping of languages to file-extensions that represent that language.
+# Feel free to add or delete extensions to any list, but don't add any new
+# compilers unless you write the interface for it yourself.
+#
+# 'rest' is reStructuredText
+# 'markdown' is MarkDown
+# 'html' assumes the file is html and just copies it
+post_compilers = {
+ "rest": ('.txt', '.rst'),
+ "markdown": ('.md', '.mdown', '.markdown'),
+ "textile": ('.textile',),
+ "txt2tags": ('.t2t',),
+ "bbcode": ('.bb',),
+ "wiki": ('.wiki',),
+ "ipynb": ('.ipynb',),
+ "html": ('.html', '.htm')
+}
+
+# Create by default posts in one file format?
+# Set to False for two-file posts, with separate metadata.
+# ONE_FILE_POSTS = True
+
+# Paths for different autogenerated bits. These are combined with the
+# translation paths.
+
+# Final locations are:
+# output / TRANSLATION[lang] / TAG_PATH / index.html (list of tags)
+# output / TRANSLATION[lang] / TAG_PATH / tag.html (list of posts for a tag)
+# output / TRANSLATION[lang] / TAG_PATH / tag.xml (RSS feed for a tag)
+# TAG_PATH = "categories"
+
+# If TAG_PAGES_ARE_INDEXES is set to True, each tag's page will contain
+# the posts themselves. If set to False, it will be just a list of links.
+# TAG_PAGES_ARE_INDEXES = True
+
+# Final location is output / TRANSLATION[lang] / INDEX_PATH / index-*.html
+# INDEX_PATH = ""
+# Final locations for the archives are:
+# output / TRANSLATION[lang] / ARCHIVE_PATH / ARCHIVE_FILENAME
+# output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / index.html
+# ARCHIVE_PATH = ""
+# ARCHIVE_FILENAME = "archive.html"
+# Final locations are:
+# output / TRANSLATION[lang] / RSS_PATH / rss.xml
+# RSS_PATH = ""
+
+# Slug the Tag URL easier for users to type, special characters are
+# often removed or replaced as well.
+# SLUG_TAG_PATH = True
+
+# A list of redirection tuples, [("foo/from.html", "/bar/to.html")].
+#
+# A HTML file will be created in output/foo/from.html that redirects
+# to the "/bar/to.html" URL. notice that the "from" side MUST be a
+# relative URL.
+#
+# If you don't need any of these, just set to []
+# REDIRECTIONS = []
+
+# Commands to execute to deploy. Can be anything, for example,
+# you may use rsync:
+# "rsync -rav output/* joe@my.site:/srv/www/site"
+# And then do a backup, or ping pingomatic.
+# To do manual deployment, set it to []
+# DEPLOY_COMMANDS = []
+
+# Where the output site should be located
+# If you don't use an absolute path, it will be considered as relative
+# to the location of conf.py
+# OUTPUT_FOLDER = 'output'
+
+# where the "cache" of partial generated content should be located
+# default: 'cache'
+# CACHE_FOLDER = 'cache'
+
+# Filters to apply to the output.
+# A directory where the keys are either: a file extensions, or
+# a tuple of file extensions.
+#
+# And the value is a list of commands to be applied in order.
+#
+# Each command must be either:
+#
+# A string containing a '%s' which will
+# be replaced with a filename. The command *must* produce output
+# in place.
+#
+# Or:
+#
+# A python callable, which will be called with the filename as
+# argument.
+#
+# By default, there are no filters.
+# FILTERS = {
+# ".jpg": ["jpegoptim --strip-all -m75 -v %s"],
+# }
+
+# Create a gzipped copy of each generated file. Cheap server-side optimization.
+# GZIP_FILES = False
+# File extensions that will be compressed
+# GZIP_EXTENSIONS = ('.txt', '.htm', '.html', '.css', '.js', '.json')
+
+# #############################################################################
+# Image Gallery Options
+# #############################################################################
+
+# Galleries are folders in galleries/
+# Final location of galleries will be output / GALLERY_PATH / gallery_name
+# GALLERY_PATH = "galleries"
+# THUMBNAIL_SIZE = 180
+# MAX_IMAGE_SIZE = 1280
+# USE_FILENAME_AS_TITLE = True
+
+# #############################################################################
+# HTML fragments and diverse things that are used by the templates
+# #############################################################################
+
+# Data about post-per-page indexes
+# INDEXES_TITLE = "" # If this is empty, the default is BLOG_TITLE
+# INDEXES_PAGES = "" # If this is empty, the default is 'old posts page %d' translated
+
+# Name of the theme to use. Themes are located in themes/theme_name
+# THEME = 'site'
+
+# If you use 'site-reveal' theme you can select several subthemes
+# THEME_REVEAL_CONGIF_SUBTHEME = 'sky' # You can also use: beige/serif/simple/night/default
+
+# Again, if you use 'site-reveal' theme you can select several transitions between the slides
+# THEME_REVEAL_CONGIF_TRANSITION = 'cube' # You can also use: page/concave/linear/none/default
+
+# date format used to display post dates. (str used by datetime.datetime.strftime)
+# DATE_FORMAT = '%Y-%m-%d %H:%M'
+
+# FAVICONS contains (name, file, size) tuples.
+# Used for create favicon link like this:
+# <link rel="name" href="file" sizes="size"/>
+# about favicons, see: http://www.netmagazine.com/features/create-perfect-favicon
+# FAVICONS = {
+# ("icon", "/favicon.ico", "16x16"),
+# ("icon", "/icon_128x128.png", "128x128"),
+# }
+
+# Show only teasers in the index pages? Defaults to False.
+# INDEX_TEASERS = False
+
+# A HTML fragment describing the license, for the sidebar. Default is "".
+# I recommend using the Creative Commons' wizard:
+# http://creativecommons.org/choose/
+# LICENSE = """
+# <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/2.5/ar/">
+# <img alt="Creative Commons License BY-NC-SA"
+# style="border-width:0; margin-bottom:12px;"
+# src="http://i.creativecommons.org/l/by-nc-sa/2.5/ar/88x31.png"></a>"""
+
+# A small copyright notice for the page footer (in HTML).
+# Default is ''
+CONTENT_FOOTER = 'Contents &copy; {date} <a href="mailto:{email}">{author}</a> - Powered by <a href="http://nikola.ralsina.com.ar">Nikola</a>'
+CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
+ author=BLOG_AUTHOR,
+ date=time.gmtime().tm_year)
+
+# To enable comments via Disqus, you need to create a forum at
+# http://disqus.com, and set DISQUS_FORUM to the short name you selected.
+# If you want to disable comments, set it to False.
+# Default is "nikolademo", used by the demo sites
+# DISQUS_FORUM = "nikolademo"
+
+# Create index.html for story folders?
+# STORY_INDEX = False
+# Enable comments on story pages?
+# COMMENTS_IN_STORIES = False
+# Enable comments on picture gallery pages?
+# COMMENTS_IN_GALLERIES = False
+
+# Do you want a add a Mathjax config file?
+# MATHJAX_CONFIG = ""
+
+# If you are using the compile-ipynb plugin, just add this one:
+#MATHJAX_CONFIG = """
+#<script type="text/x-mathjax-config">
+#MathJax.Hub.Config({
+# tex2jax: {
+# inlineMath: [ ['$','$'], ["\\\(","\\\)"] ],
+# displayMath: [ ['$$','$$'], ["\\\[","\\\]"] ]
+# },
+# displayAlign: 'left', // Change this to 'center' to center equations.
+# "HTML-CSS": {
+# styles: {'.MathJax_Display': {"margin": 0}}
+# }
+#});
+#</script>
+#"""
+
+# Enable Addthis social buttons?
+# Defaults to true
+# ADD_THIS_BUTTONS = True
+
+# Modify the number of Post per Index Page
+# Defaults to 10
+# INDEX_DISPLAY_POST_COUNT = 10
+
+# RSS_LINK is a HTML fragment to link the RSS or Atom feeds. If set to None,
+# the base.tmpl will use the feed Nikola generates. However, you may want to
+# change it for a feedburner feed or something else.
+# RSS_LINK = None
+
+# Show only teasers in the RSS feed? Default to True
+# RSS_TEASERS = True
+
+# A search form to search this site, for the sidebar. You can use a google
+# custom search (http://www.google.com/cse/)
+# Or a duckduckgo search: https://duckduckgo.com/search_box.html
+# Default is no search form.
+# SEARCH_FORM = ""
+#
+# This search form works for any site and looks good in the "site" theme where it
+# appears on the navigation bar
+#SEARCH_FORM = """
+#<!-- Custom search -->
+#<form method="get" id="search" action="http://duckduckgo.com/"
+# class="navbar-form pull-left">
+#<input type="hidden" name="sites" value="%s"/>
+#<input type="hidden" name="k8" value="#444444"/>
+#<input type="hidden" name="k9" value="#D51920"/>
+#<input type="hidden" name="kt" value="h"/>
+#<input type="text" name="q" maxlength="255"
+# placeholder="Search&hellip;" class="span2" style="margin-top: 4px;"/>
+#<input type="submit" value="DuckDuckGo Search" style="visibility: hidden;" />
+#</form>
+#<!-- End of custom search -->
+#""" % BLOG_URL
+#
+# Also, there is a local search plugin you can use.
+
+# Use content distribution networks for jquery and twitter-bootstrap css and js
+# If this is True, jquery is served from the Google CDN and twitter-bootstrap
+# is served from the NetDNA CDN
+# Set this to False if you want to host your site without requiring access to
+# external resources.
+# USE_CDN = False
+
+# Google analytics or whatever else you use. Added to the bottom of <body>
+# in the default template (base.tmpl).
+# ANALYTICS = ""
+
+# The possibility to extract metadata from the filename by using a
+# regular expression.
+# To make it work you need to name parts of your regular expression.
+# The following names will be used to extract metadata:
+# - title
+# - slug
+# - date
+# - tags
+# - link
+# - description
+#
+# An example re is the following:
+# '(?P<date>\d{4}-\d{2}-\d{2})-(?P<slug>.*)-(?P<title>.*)\.md'
+# FILE_METADATA_REGEXP = None
+
+# Nikola supports Twitter Card summaries / Open Graph.
+# Twitter cards make it possible for you to attach media to Tweets
+# that link to your content.
+#
+# IMPORTANT:
+# Please note, that you need to opt-in for using Twitter Cards!
+# To do this please visit https://dev.twitter.com/form/participate-twitter-cards
+#
+# Uncomment and modify to following lines to match your accounts.
+# Specifying the id for either 'site' or 'creator' will be preferred
+# over the cleartext username. Specifying an ID is not necessary.
+# Displaying images is currently not supported.
+# TWITTER_CARD = {
+# # 'use_twitter_cards': True, # enable Twitter Cards / Open Graph
+# # 'site': '@website', # twitter nick for the website
+# # 'site:id': 123456, # Same as site, but the website's Twitter user ID instead.
+# # 'creator': '@username', # Username for the content creator / author.
+# # 'creator:id': 654321, # Same as creator, but the Twitter user's ID.
+# }
+
+
+# If you want to use formatted post time in W3C-DTF Format(ex. 2012-03-30T23:00:00+02:00),
+# set timzone if you want a localized posted date.
+#
+# TIMEZONE = 'Europe/Zurich'
+
+# If webassets is installed, bundle JS and CSS to make site loading faster
+# USE_BUNDLES = True
+
+# Plugins you don't want to use. Be careful :-)
+# DISABLED_PLUGINS = ["render_galleries"]
+
+# Put in global_context things you want available on all your templates.
+# It can be anything, data, functions, modules, etc.
+
+GLOBAL_CONTEXT = {}
diff --git a/tests/data/translated_titles/stories/1.txt b/tests/data/translated_titles/stories/1.txt
new file mode 100644
index 0000000..45fb214
--- /dev/null
+++ b/tests/data/translated_titles/stories/1.txt
@@ -0,0 +1,5 @@
+.. title: Foo
+.. slug: 1
+.. date: 2001/01/01 00:00:00
+
+Foo
diff --git a/tests/data/translated_titles/stories/1.txt.es b/tests/data/translated_titles/stories/1.txt.es
new file mode 100644
index 0000000..a888c1f
--- /dev/null
+++ b/tests/data/translated_titles/stories/1.txt.es
@@ -0,0 +1,4 @@
+.. title: Bar
+.. slug: 1
+
+Bar
diff --git a/tests/test_command_import_wordpress.py b/tests/test_command_import_wordpress.py
index bda9b49..3be2ad9 100644
--- a/tests/test_command_import_wordpress.py
+++ b/tests/test_command_import_wordpress.py
@@ -9,17 +9,38 @@ import mock
class BasicCommandImportWordpress(unittest.TestCase):
def setUp(self):
- self.import_command = nikola.plugins.command_import_wordpress.CommandImportWordpress(
- )
- self.import_filename = os.path.abspath(
- os.path.join(os.path.dirname(__file__),
- 'wordpress_export_example.xml'))
+ self.import_command = nikola.plugins.command_import_wordpress.CommandImportWordpress()
+ self.import_filename = os.path.abspath(os.path.join(
+ os.path.dirname(__file__), 'wordpress_export_example.xml'))
def tearDown(self):
del self.import_command
del self.import_filename
+class TestXMLGlueing(BasicCommandImportWordpress):
+ def test_making_correct_newlines(self):
+ xml = [b"Some information about how to (un)subscripe to a google group with a normal mail client.\n",
+ b"<ul>\n",
+ b" <li>to post: <strong>groupname@googlegroups.com</strong></li>\n",
+ b" <li>to <em>subscribe</em>: <strong>groupname+subscribe@googlegroups.com</strong></li>\n",
+ b" <li>to <em>unsubscribe</em>: <strong>groupname+unsubscribe@googlegroups.com</strong></li>\n",
+ b"</ul>\n",
+ b"Easy.\n"]
+
+ expected_xml = b"""Some information about how to (un)subscripe to a google group with a normal mail client.
+
+<ul>
+ <li>to post: <strong>groupname@googlegroups.com</strong></li>
+ <li>to <em>subscribe</em>: <strong>groupname+subscribe@googlegroups.com</strong></li>
+ <li>to <em>unsubscribe</em>: <strong>groupname+unsubscribe@googlegroups.com</strong></li>
+</ul>
+
+Easy.
+"""
+ self.assertEqual(expected_xml, self.import_command._glue_xml_lines(xml))
+
+
class CommandImportWordpressRunTest(BasicCommandImportWordpress):
def setUp(self):
super(self.__class__, self).setUp()
@@ -28,8 +49,7 @@ class CommandImportWordpressRunTest(BasicCommandImportWordpress):
self.write_urlmap = mock.MagicMock()
self.write_configuration = mock.MagicMock()
- site_generation_patch = mock.patch(
- 'nikola.plugins.command_import_wordpress.CommandImportWordpress.generate_base_site', self.site_generation)
+ site_generation_patch = mock.patch('os.system', self.site_generation)
data_import_patch = mock.patch(
'nikola.plugins.command_import_wordpress.CommandImportWordpress.import_posts', self.data_import)
write_urlmap_patch = mock.patch(
@@ -56,14 +76,14 @@ class CommandImportWordpressRunTest(BasicCommandImportWordpress):
def test_create_import(self):
valid_import_arguments = (
- ['--filename', self.import_filename],
- ['-f', self.import_filename, '-o', 'some_folder'],
- [self.import_filename],
- [self.import_filename, 'folder_argument'],
+ dict(options={'output_folder': 'some_folder'},
+ args=[self.import_filename]),
+ dict(args=[self.import_filename]),
+ dict(args=[self.import_filename, 'folder_argument']),
)
for arguments in valid_import_arguments:
- self.import_command.run(*arguments)
+ self.import_command.execute(**arguments)
self.assertTrue(self.site_generation.called)
self.assertTrue(self.data_import.called)
@@ -73,29 +93,24 @@ class CommandImportWordpressRunTest(BasicCommandImportWordpress):
def test_ignoring_drafts(self):
valid_import_arguments = (
- ['--filename', self.import_filename, '--no-drafts'],
- ['-f', self.import_filename, '-o', 'some_folder', '-d'],
+ dict(options={'exclude_drafts': True}, args=[
+ self.import_filename]),
+ dict(
+ options={'exclude_drafts': True,
+ 'output_folder': 'some_folder'},
+ args=[self.import_filename]),
)
for arguments in valid_import_arguments:
- self.import_command.run(*arguments)
+ self.import_command.execute(**arguments)
self.assertTrue(self.import_command.exclude_drafts)
- def test_getting_help(self):
- for arguments in (['-h'], ['--help']):
- self.assertRaises(SystemExit, self.import_command.run, *arguments)
-
- self.assertFalse(self.site_generation.called)
- self.assertFalse(self.data_import.called)
- self.assertFalse(self.write_urlmap.called)
- self.assertFalse(self.write_configuration.called)
-
class CommandImportWordpressTest(BasicCommandImportWordpress):
def test_create_import_work_without_argument(self):
# Running this without an argument must not fail.
# It should show the proper usage of the command.
- self.import_command.run()
+ self.import_command.execute()
def test_populate_context(self):
channel = self.import_command.get_channel_from_file(
@@ -109,7 +124,7 @@ class CommandImportWordpressTest(BasicCommandImportWordpress):
self.assertEqual('Wordpress blog title', context['BLOG_TITLE'])
self.assertEqual('Nikola test blog ;) - with moré Ümläüts',
context['BLOG_DESCRIPTION'])
- self.assertEqual('http://some.blog', context['BLOG_URL'])
+ self.assertEqual('http://some.blog', context['SITE_URL'])
self.assertEqual('mail@some.blog', context['BLOG_EMAIL'])
self.assertEqual('Niko', context['BLOG_AUTHOR'])
@@ -120,6 +135,8 @@ class CommandImportWordpressTest(BasicCommandImportWordpress):
channel)
self.import_command.url_map = {} # For testing we use an empty one.
self.import_command.output_folder = 'new_site'
+ self.import_command.squash_newlines = True
+ self.import_command.no_downloads = False
write_metadata = mock.MagicMock()
write_content = mock.MagicMock()
@@ -142,11 +159,42 @@ class CommandImportWordpressTest(BasicCommandImportWordpress):
'kontakt', '2009-07-16 20:20:32', None, [])
self.assertTrue(write_content.called)
- write_content.assert_any_call('new_site/posts/200704hoert.wp', 'An image.\n\n\n\n<img class="size-full wp-image-16" title="caption test" src="http://some.blog/wp-content/uploads/2009/07/caption_test.jpg" alt="caption test" width="739" height="517" />\n\n\n\nSome source code.\n\n\n\n\n~~~~~~~~~~~~{.Python}\n\n\nimport sys\n\nprint sys.version\n\n\n~~~~~~~~~~~~\n\n\n\n\nThe end.\n\n')
+ write_content.assert_any_call('new_site/posts/200704hoert.wp',
+ """An image.
+
+<img class="size-full wp-image-16" title="caption test" src="http://some.blog/wp-content/uploads/2009/07/caption_test.jpg" alt="caption test" width="739" height="517" />
+
+Some source code.
+
+~~~~~~~~~~~~{.Python}
+
+import sys
+
+print sys.version
+
+~~~~~~~~~~~~
+
+The end.
+
+""")
+
write_content.assert_any_call(
- 'new_site/posts/200807arzt-und-pfusch-s-i-c-k.wp', '<img class="size-full wp-image-10 alignright" title="Arzt+Pfusch - S.I.C.K." src="http://some.blog/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png" alt="Arzt+Pfusch - S.I.C.K." width="210" height="209" />Arzt+Pfusch - S.I.C.K.Gerade bin ich \xfcber das Album <em>S.I.C.K</em> von <a title="Arzt+Pfusch" href="http://www.arztpfusch.com/" target="_blank">Arzt+Pfusch</a> gestolpert, welches Arzt+Pfusch zum Download f\xfcr lau anbieten. Das Album steht unter einer Creative Commons <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/de/">BY-NC-ND</a>-Lizenz.\n\nDie Ladung <em>noisebmstupidevildustrial</em> gibts als MP3s mit <a href="http://www.archive.org/download/dmp005/dmp005_64kb_mp3.zip">64kbps</a> und <a href="http://www.archive.org/download/dmp005/dmp005_vbr_mp3.zip">VBR</a>, als Ogg Vorbis und als FLAC (letztere <a href="http://www.archive.org/details/dmp005">hier</a>). <a href="http://www.archive.org/download/dmp005/dmp005-artwork.zip">Artwork</a> und <a href="http://www.archive.org/download/dmp005/dmp005-lyrics.txt">Lyrics</a> gibts nochmal einzeln zum Download.')
+ 'new_site/posts/200807arzt-und-pfusch-s-i-c-k.wp',
+ '''<img class="size-full wp-image-10 alignright" title="Arzt+Pfusch - S.I.C.K." src="http://some.blog/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png" alt="Arzt+Pfusch - S.I.C.K." width="210" height="209" />Arzt+Pfusch - S.I.C.K.Gerade bin ich \xfcber das Album <em>S.I.C.K</em> von <a title="Arzt+Pfusch" href="http://www.arztpfusch.com/" target="_blank">Arzt+Pfusch</a> gestolpert, welches Arzt+Pfusch zum Download f\xfcr lau anbieten. Das Album steht unter einer Creative Commons <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/de/">BY-NC-ND</a>-Lizenz.
+Die Ladung <em>noisebmstupidevildustrial</em> gibts als MP3s mit <a href="http://www.archive.org/download/dmp005/dmp005_64kb_mp3.zip">64kbps</a> und <a href="http://www.archive.org/download/dmp005/dmp005_vbr_mp3.zip">VBR</a>, als Ogg Vorbis und als FLAC (letztere <a href="http://www.archive.org/details/dmp005">hier</a>). <a href="http://www.archive.org/download/dmp005/dmp005-artwork.zip">Artwork</a> und <a href="http://www.archive.org/download/dmp005/dmp005-lyrics.txt">Lyrics</a> gibts nochmal einzeln zum Download.''')
write_content.assert_any_call(
- 'new_site/stories/kontakt.wp', '<h1>Datenschutz</h1>\n\nIch erhebe und speichere automatisch in meine Server Log Files Informationen, die dein Browser an mich \xfcbermittelt. Dies sind:\n\n<ul>\n\n <li>Browsertyp und -version</li>\n\n <li>verwendetes Betriebssystem</li>\n\n <li>Referrer URL (die zuvor besuchte Seite)</li>\n\n <li>IP Adresse des zugreifenden Rechners</li>\n\n <li>Uhrzeit der Serveranfrage.</li>\n\n</ul>\n\nDiese Daten sind f\xfcr mich nicht bestimmten Personen zuordenbar. Eine Zusammenf\xfchrung dieser Daten mit anderen Datenquellen wird nicht vorgenommen, die Daten werden einzig zu statistischen Zwecken erhoben.')
+ 'new_site/stories/kontakt.wp', """<h1>Datenschutz</h1>
+Ich erhebe und speichere automatisch in meine Server Log Files Informationen, die dein Browser an mich \xfcbermittelt. Dies sind:
+
+<ul>
+ <li>Browsertyp und -version</li>
+ <li>verwendetes Betriebssystem</li>
+ <li>Referrer URL (die zuvor besuchte Seite)</li>
+ <li>IP Adresse des zugreifenden Rechners</li>
+ <li>Uhrzeit der Serveranfrage.</li>
+</ul>
+
+Diese Daten sind f\xfcr mich nicht bestimmten Personen zuordenbar. Eine Zusammenf\xfchrung dieser Daten mit anderen Datenquellen wird nicht vorgenommen, die Daten werden einzig zu statistischen Zwecken erhoben.""")
self.assertTrue(len(self.import_command.url_map) > 0)
@@ -165,13 +213,16 @@ class CommandImportWordpressTest(BasicCommandImportWordpress):
"""Applying markup conversions to content."""
transform_sourcecode = mock.MagicMock()
transform_caption = mock.MagicMock()
+ transform_newlines = mock.MagicMock()
with mock.patch('nikola.plugins.command_import_wordpress.CommandImportWordpress.transform_sourcecode', transform_sourcecode):
with mock.patch('nikola.plugins.command_import_wordpress.CommandImportWordpress.transform_caption', transform_caption):
- self.import_command.transform_content("random content")
+ with mock.patch('nikola.plugins.command_import_wordpress.CommandImportWordpress.transform_multiple_newlines', transform_newlines):
+ self.import_command.transform_content("random content")
self.assertTrue(transform_sourcecode.called)
self.assertTrue(transform_caption.called)
+ self.assertTrue(transform_newlines.called)
def test_transforming_source_code(self):
"""
@@ -226,6 +277,37 @@ asdasdas"""
self.assertEqual(
expected_content, self.import_command.transform_caption(content))
+ def test_transform_multiple_newlines(self):
+ content = """This
+
+
+has
+
+
+
+way to many
+
+newlines.
+
+
+"""
+ expected_content = """This
+
+has
+
+way to many
+
+newlines.
+
+"""
+ self.import_command.squash_newlines = False
+ self.assertEqual(content,
+ self.import_command.transform_multiple_newlines(content))
+
+ self.import_command.squash_newlines = True
+ self.assertEqual(expected_content,
+ self.import_command.transform_multiple_newlines(content))
+
def test_transform_caption_with_link_inside(self):
content = """[caption caption="Fehlermeldung"]<a href="http://some.blog/openttd-missing_sound.png"><img class="size-thumbnail wp-image-551" title="openttd-missing_sound" src="http://some.blog/openttd-missing_sound-150x150.png" alt="Fehlermeldung" /></a>[/caption]"""
transformed_content = self.import_command.transform_caption(content)
diff --git a/tests/test_command_init.py b/tests/test_command_init.py
index 32ce345..1904fa1 100644
--- a/tests/test_command_init.py
+++ b/tests/test_command_init.py
@@ -35,27 +35,26 @@ class CommandInitCallTest(unittest.TestCase):
del self.create_empty_site
def test_init_default(self):
- for arguments in (('destination', '--demo'),):
- self.init_commad.run(*arguments)
+ for arguments in (dict(options={'demo': True}, args=['destination']), {}):
+ self.init_commad.execute(**arguments)
self.assertTrue(self.create_configuration.called)
self.assertTrue(self.copy_sample_site.called)
self.assertFalse(self.create_empty_site.called)
def test_init_called_without_target(self):
- self.init_commad.run()
+ self.init_commad.execute()
self.assertFalse(self.create_configuration.called)
self.assertFalse(self.copy_sample_site.called)
self.assertFalse(self.create_empty_site.called)
def test_init_empty_dir(self):
- for arguments in (('destination', ), ('destination', '--empty')):
- self.init_commad.run(*arguments)
+ self.init_commad.execute(args=['destination'])
- self.assertTrue(self.create_configuration.called)
- self.assertFalse(self.copy_sample_site.called)
- self.assertTrue(self.create_empty_site.called)
+ self.assertTrue(self.create_configuration.called)
+ self.assertFalse(self.copy_sample_site.called)
+ self.assertTrue(self.create_empty_site.called)
if __name__ == '__main__':
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 947a832..c940a07 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -1,13 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
+import codecs
from contextlib import contextmanager
import os
import shutil
import tempfile
import unittest
+import lxml.html
+
from context import nikola
+from nikola import main
@contextmanager
@@ -18,56 +22,147 @@ def cd(path):
os.chdir(old_dir)
-class IntegrationTest(unittest.TestCase):
+class EmptyBuildTest(unittest.TestCase):
"""Basic integration testcase."""
- def setUp(self):
+
+ dataname = None
+
+ @classmethod
+ def setUpClass(self):
"""Setup a demo site."""
self.tmpdir = tempfile.mkdtemp()
self.target_dir = os.path.join(self.tmpdir, "target")
- self.build_command = nikola.plugins.command_build.CommandBuild()
self.init_command = nikola.plugins.command_init.CommandInit()
- self.init_command.copy_sample_site(self.target_dir)
- self.init_command.create_configuration(self.target_dir)
+ self.fill_site()
self.patch_site()
self.build()
+ @classmethod
+ def fill_site(self):
+ """Add any needed initial content."""
+ self.init_command.create_empty_site(self.target_dir)
+ self.init_command.create_configuration(self.target_dir)
+
+ if self.dataname:
+ src = os.path.join(os.path.dirname(__file__), 'data',
+ self.dataname)
+ for root, dirs, files in os.walk(src):
+ for src_name in files:
+ rel_dir = os.path.relpath(root, src)
+ dst_file = os.path.join(self.target_dir, rel_dir, src_name)
+ src_file = os.path.join(root, src_name)
+ shutil.copy2(src_file, dst_file)
+
+ @classmethod
def patch_site(self):
"""Make any modifications you need to the site."""
- pass
+ @classmethod
def build(self):
"""Build the site."""
with cd(self.target_dir):
- self.build_command.run()
+ main.main(["build"])
- def tearDown(self):
- """Reove the demo site."""
- shutil.rmtree(self.tmpdir)
+ @classmethod
+ def tearDownClass(self):
+ """Remove the demo site."""
+ def test_build(self):
+ """Ensure the build did something."""
+ index_path = os.path.join(
+ self.target_dir, "output", "archive.html")
+ self.assertTrue(os.path.isfile(index_path))
-class EmptytBuild(IntegrationTest):
- """Basic integration testcase."""
- def setUp(self):
- """Setup a demo site."""
- self.tmpdir = tempfile.mkdtemp()
- self.target_dir = os.path.join(self.tmpdir, "target")
- self.build_command = nikola.plugins.command_build.CommandBuild()
- self.init_command = nikola.plugins.command_init.CommandInit()
- self.init_command.create_empty_site(self.target_dir)
+
+class DemoBuildTest(EmptyBuildTest):
+ """Test that a default build of --demo works."""
+
+ @classmethod
+ def fill_site(self):
+ """Fill the site with demo content."""
+ self.init_command.copy_sample_site(self.target_dir)
self.init_command.create_configuration(self.target_dir)
- self.patch_site()
- self.build()
- def test_deleted_dodo(self):
- """Test that a default build of --demo works."""
- # Ensure the temprary dodo file is deleted (Issue #302)
- self.assertFalse(os.path.isfile(self.build_command.dodo.name))
+class TranslatedBuildTest(EmptyBuildTest):
+ """Test a site with translated content."""
-class DefaultBuild(IntegrationTest):
- """Test that a default build of --demo works."""
+ dataname = "translated_titles"
+
+ def test_translated_titles(self):
+ """Check that translated title is picked up."""
+ en_file = os.path.join(self.target_dir, "output", "stories", "1.html")
+ es_file = os.path.join(self.target_dir, "output", "es", "stories", "1.html")
+ # Files should be created
+ self.assertTrue(os.path.isfile(en_file))
+ self.assertTrue(os.path.isfile(es_file))
+ # And now let's check the titles
+ with codecs.open(en_file, 'r', 'utf8') as inf:
+ doc = lxml.html.parse(inf)
+ self.assertEqual(doc.find('//title').text, 'Foo | Demo Site')
+ with codecs.open(es_file, 'r', 'utf8') as inf:
+ doc = lxml.html.parse(inf)
+ self.assertEqual(doc.find('//title').text, 'Bar | Demo Site')
+
+
+class RelativeLinkTest(DemoBuildTest):
+ """Check that SITE_URL with a path doesn't break links."""
+
+ @classmethod
+ def patch_site(self):
+ """Set the SITE_URL to have a path"""
+ conf_path = os.path.join(self.target_dir, "conf.py")
+ with codecs.open(conf_path, "rb", "utf-8") as inf:
+ data = inf.read()
+ data = data.replace('SITE_URL = "http://nikola.ralsina.com.ar"',
+ 'SITE_URL = "http://nikola.ralsina.com.ar/foo/bar/"')
+ with codecs.open(conf_path, "wb+", "utf8") as outf:
+ outf.write(data)
+
+ def test_relative_links(self):
+ """Check that the links in output/index.html are correct"""
+ test_path = os.path.join(self.target_dir, "output", "index.html")
+ flag = False
+ with open(test_path, "rb") as inf:
+ data = inf.read()
+ for _, _, url, _ in lxml.html.iterlinks(data):
+ # Just need to be sure this one is ok
+ if url.endswith("css"):
+ self.assertFalse(url.startswith(".."))
+ flag = True
+ # But I also need to be sure it is there!
+ self.assertTrue(flag)
+
+
+class RelativeLinkTest2(DemoBuildTest):
+ """Check that dropping stories to the root doesn't break links."""
+
+ @classmethod
+ def patch_site(self):
+ """Set the SITE_URL to have a path"""
+ conf_path = os.path.join(self.target_dir, "conf.py")
+ with codecs.open(conf_path, "rb", "utf-8") as inf:
+ data = inf.read()
+ data = data.replace('("stories/*.txt", "stories", "story.tmpl", False),',
+ '("stories/*.txt", "", "story.tmpl", False),')
+ data = data.replace('# INDEX_PATH = ""',
+ 'INDEX_PATH = "blog"')
+ with codecs.open(conf_path, "wb+", "utf8") as outf:
+ outf.write(data)
+ outf.flush()
- def test_deleted_dodo(self):
- """Test that a default build of --demo works."""
- # Ensure the temprary dodo file is deleted (Issue #302)
- self.assertFalse(os.path.isfile(self.build_command.dodo.name))
+ def test_relative_links(self):
+ """Check that the links in a story are correct"""
+ conf_path = os.path.join(self.target_dir, "conf.py")
+ data = open(conf_path).read()
+ test_path = os.path.join(self.target_dir, "output", "about-nikola.html")
+ flag = False
+ with open(test_path, "rb") as inf:
+ data = inf.read()
+ for _, _, url, _ in lxml.html.iterlinks(data):
+ # Just need to be sure this one is ok
+ if url.endswith("css"):
+ self.assertFalse(url.startswith(".."))
+ flag = True
+ # But I also need to be sure it is there!
+ self.assertTrue(flag)
diff --git a/tests/test_rss_feeds.py b/tests/test_rss_feeds.py
index ae1cd41..5b9b981 100644
--- a/tests/test_rss_feeds.py
+++ b/tests/test_rss_feeds.py
@@ -16,11 +16,13 @@ class RSSFeedTest(unittest.TestCase):
def setUp(self):
self.blog_url = "http://some.blog"
- with mock.patch('nikola.nikola.utils.get_meta',
- mock.Mock(return_value=('post title',
- 'awesome_article',
- '2012-10-01 22:41', 'tags',
- 'link', 'description'))):
+ with mock.patch('nikola.post.get_meta',
+ mock.Mock(return_value=({'title': 'post title',
+ 'slug': 'awesome_article',
+ 'date': '2012-10-01 22:41',
+ 'tags': 'tags', 'link':
+ 'link', 'description':
+ 'description'}))):
with mock.patch('nikola.nikola.utils.os.path.isdir',
mock.Mock(return_value=True)):
with mock.patch('nikola.nikola.Post.text',
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 2cb36a8..4f3fd72 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,9 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-from context import nikola
import unittest
import mock
+from nikola.post import get_meta
+
+
+class dummy(object):
+ pass
class GetMetaTest(unittest.TestCase):
@@ -19,16 +23,19 @@ class GetMetaTest(unittest.TestCase):
opener_mock = mock.mock_open(read_data=file_metadata)
opener_mock.return_value.readlines.return_value = file_metadata
- with mock.patch('nikola.utils.codecs.open', opener_mock, create=True):
- (title, slug, date, tags, link,
- description) = nikola.utils.get_meta('file_with_metadata')
+ post = dummy()
+ post.source_path = 'file_with_metadata'
+ post.metadata_path = 'file_with_metadata.meta'
- self.assertEqual('Nikola needs more tests!', title)
- self.assertEqual('write-tests-now', slug)
- self.assertEqual('2012/09/15 19:52:05', date)
- self.assertEqual('', tags)
- self.assertEqual('', link)
- self.assertEqual('', description)
+ with mock.patch('nikola.post.codecs.open', opener_mock, create=True):
+ meta = get_meta(post)
+
+ self.assertEqual('Nikola needs more tests!', meta['title'])
+ self.assertEqual('write-tests-now', meta['slug'])
+ self.assertEqual('2012/09/15 19:52:05', meta['date'])
+ self.assertFalse('tags' in meta)
+ self.assertFalse('link' in meta)
+ self.assertFalse('description' in meta)
def test_get_title_from_rest(self):
file_metadata = [".. slug: write-tests-now\n",
@@ -42,16 +49,19 @@ class GetMetaTest(unittest.TestCase):
opener_mock = mock.mock_open(read_data=file_metadata)
opener_mock.return_value.readlines.return_value = file_metadata
- with mock.patch('nikola.utils.codecs.open', opener_mock, create=True):
- (title, slug, date, tags, link,
- description) = nikola.utils.get_meta('file_with_metadata')
+ post = dummy()
+ post.source_path = 'file_with_metadata'
+ post.metadata_path = 'file_with_metadata.meta'
+
+ with mock.patch('nikola.post.codecs.open', opener_mock, create=True):
+ meta = get_meta(post)
- self.assertEqual('Post Title', title)
- self.assertEqual('write-tests-now', slug)
- self.assertEqual('2012/09/15 19:52:05', date)
- self.assertEqual('', tags)
- self.assertEqual('', link)
- self.assertEqual('', description)
+ self.assertEqual('Post Title', meta['title'])
+ self.assertEqual('write-tests-now', meta['slug'])
+ self.assertEqual('2012/09/15 19:52:05', meta['date'])
+ self.assertFalse('tags' in meta)
+ self.assertFalse('link' in meta)
+ self.assertFalse('description' in meta)
def test_get_title_from_fname(self):
file_metadata = [".. slug: write-tests-now\n",
@@ -63,16 +73,19 @@ class GetMetaTest(unittest.TestCase):
opener_mock = mock.mock_open(read_data=file_metadata)
opener_mock.return_value.readlines.return_value = file_metadata
- with mock.patch('nikola.utils.codecs.open', opener_mock, create=True):
- (title, slug, date, tags, link,
- description) = nikola.utils.get_meta('file_with_metadata')
+ post = dummy()
+ post.source_path = 'file_with_metadata'
+ post.metadata_path = 'file_with_metadata.meta'
- self.assertEqual('file_with_metadata', title)
- self.assertEqual('write-tests-now', slug)
- self.assertEqual('2012/09/15 19:52:05', date)
- self.assertEqual('', tags)
- self.assertEqual('', link)
- self.assertEqual('', description)
+ with mock.patch('nikola.post.codecs.open', opener_mock, create=True):
+ meta = get_meta(post, 'file_with_metadata')
+
+ self.assertEqual('file_with_metadata', meta['title'])
+ self.assertEqual('write-tests-now', meta['slug'])
+ self.assertEqual('2012/09/15 19:52:05', meta['date'])
+ self.assertFalse('tags' in meta)
+ self.assertFalse('link' in meta)
+ self.assertFalse('description' in meta)
def test_use_filename_as_slug_fallback(self):
file_metadata = [".. title: Nikola needs more tests!\n",
@@ -85,36 +98,40 @@ class GetMetaTest(unittest.TestCase):
opener_mock = mock.mock_open(read_data=file_metadata)
opener_mock.return_value.readlines.return_value = file_metadata
- with mock.patch('nikola.utils.codecs.open', opener_mock, create=True):
- (title, slug, date, tags, link,
- description) = nikola.utils.get_meta('Slugify this')
+ post = dummy()
+ post.source_path = 'Slugify this'
+ post.metadata_path = 'Slugify this.meta'
+
+ with mock.patch('nikola.post.codecs.open', opener_mock, create=True):
+ meta = get_meta(post, 'Slugify this')
- self.assertEqual('Nikola needs more tests!', title)
- self.assertEqual('slugify-this', slug)
- self.assertEqual('2012/09/15 19:52:05', date)
- self.assertEqual('', tags)
- self.assertEqual('', link)
- self.assertEqual('', description)
+ self.assertEqual('Nikola needs more tests!', meta['title'])
+ self.assertEqual('slugify-this', meta['slug'])
+ self.assertEqual('2012/09/15 19:52:05', meta['date'])
+ self.assertFalse('tags' in meta)
+ self.assertFalse('link' in meta)
+ self.assertFalse('description' in meta)
def test_extracting_metadata_from_filename(self):
- with mock.patch('nikola.utils.codecs.open', create=True):
- (
- title, slug, date, tags, link, description) = nikola.utils.get_meta('2013-01-23-the_slug-dubdubtitle.md',
- '(?P<date>\d{4}-\d{2}-\d{2})-(?P<slug>.*)-(?P<title>.*)\.md')
-
- self.assertEqual('dubdubtitle', title)
- self.assertEqual('the_slug', slug)
- self.assertEqual('2013-01-23', date)
- self.assertEqual('', tags)
- self.assertEqual('', link)
- self.assertEqual('', description)
+ post = dummy()
+ post.source_path = '2013-01-23-the_slug-dubdubtitle.md'
+ post.metadata_path = '2013-01-23-the_slug-dubdubtitle.meta'
+ with mock.patch('nikola.post.codecs.open', create=True):
+ meta = get_meta(post,
+ '(?P<date>\d{4}-\d{2}-\d{2})-(?P<slug>.*)-(?P<title>.*)\.md')
+
+ self.assertEqual('dubdubtitle', meta['title'])
+ self.assertEqual('the_slug', meta['slug'])
+ self.assertEqual('2013-01-23', meta['date'])
def test_get_meta_slug_only_from_filename(self):
- with mock.patch('nikola.utils.codecs.open', create=True):
- (title, slug, date, tags, link,
- description) = nikola.utils.get_meta('some/path/the_slug.md')
+ post = dummy()
+ post.source_path = 'some/path/the_slug.md'
+ post.metadata_path = 'some/path/the_slug.meta'
+ with mock.patch('nikola.post.codecs.open', create=True):
+ meta = get_meta(post)
- self.assertEqual('the_slug', slug)
+ self.assertEqual('the_slug', meta['slug'])
if __name__ == '__main__':
unittest.main()
diff --git a/tests/wordpress_export_example.xml b/tests/wordpress_export_example.xml
index 8ef1325..e697a5b 100644
--- a/tests/wordpress_export_example.xml
+++ b/tests/wordpress_export_example.xml
@@ -177,6 +177,58 @@ Diese Daten sind für mich nicht bestimmten Personen zuordenbar. Eine ZusammenfÃ
<wp:meta_value><![CDATA[default]]></wp:meta_value>
</wp:postmeta>
</item>
+ <item>
+ <title>Indentation Test</title>
+ <link>http://some.blog/2012/04/indentation_test/</link>
+ <pubDate>Sun, 15 Apr 2012 11:44:59 +0000</pubDate>
+ <dc:creator>Niko</dc:creator>
+ <guid isPermaLink="false">http://some.blog/?p=2077</guid>
+ <description></description>
+ <content:encoded><![CDATA[Some examples for indented code that should not be broken.
+
+You should see some Python code hereafter. The code should be one block.
+<pre>class Borg:
+ _state = {}
+ def __init__(self):
+ self.__dict__ = self._state</pre>
+&nbsp;
+
+Here is a listing made with HTML that should display without the HTML being visible to the visitor.
+<ul>
+ <li>to post: <strong>groupname@googlegroups.com</strong></li>
+ <li>to <em>subscribe</em>: <strong>groupname+subscribe@googlegroups.com</strong></li>
+ <li>to <em>unsubscribe</em>: <strong>groupname+unsubscribe@googlegroups.com</strong></li>
+</ul>
+
+A listing with another listing inside.
+<ul>
+<li> foo
+ <ul>
+ <li> bar
+ </ul>
+</ul>
+]]></content:encoded>
+ <excerpt:encoded><![CDATA[]]></excerpt:encoded>
+ <wp:post_id>2077</wp:post_id>
+ <wp:post_date>2012-04-15 12:44:59</wp:post_date>
+ <wp:post_date_gmt>2012-04-15 11:44:59</wp:post_date_gmt>
+ <wp:comment_status>open</wp:comment_status>
+ <wp:ping_status>open</wp:ping_status>
+ <wp:post_name>python-borg-pattern</wp:post_name>
+ <wp:status>publish</wp:status>
+ <wp:post_parent>0</wp:post_parent>
+ <wp:menu_order>0</wp:menu_order>
+ <wp:post_type>post</wp:post_type>
+ <wp:post_password></wp:post_password>
+ <wp:is_sticky>0</wp:is_sticky>
+ <category domain="category" nicename="programming"><![CDATA[programming]]></category>
+ <category domain="post_tag" nicename="design-patterns"><![CDATA[Design Patterns]]></category>
+ <category domain="post_tag" nicename="python"><![CDATA[Python]]></category>
+ <wp:postmeta>
+ <wp:meta_key>_edit_last</wp:meta_key>
+ <wp:meta_value><![CDATA[2]]></wp:meta_value>
+ </wp:postmeta>
+ </item>
</channel>
</rss>