diff --git a/addressable_paragraphs/addressable_paragraphs.py b/addressable_paragraphs/addressable_paragraphs.py index 6adf8dc..55c375c 100644 --- a/addressable_paragraphs/addressable_paragraphs.py +++ b/addressable_paragraphs/addressable_paragraphs.py @@ -1,8 +1,24 @@ """ Addressable Paragraphs ------------------------ -In converting from MD to html images are wrapped in

objects. +In converting from .md to .html, images are wrapped in

tags. This plugin gives those paragraphs the 'img' class for styling enhancements. +In case there is any description immediately following that image, it is wrapped in another paragraph with the 'caption' class. + +Copyright (C) 2018 Roel Roscam Abbing + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . """ from __future__ import unicode_literals @@ -18,11 +34,12 @@ def content_object_init(instance): for p in soup(['p', 'object']): if p.findChild('img'): p.attrs['class'] = 'img' - caption = soup.new_tag('span',**{'class':'caption'}) - for i in reversed(p.contents): - if i.name != 'img': #if it is not an tag - caption.insert(0,i.extract()) - p.append(caption) + caption = soup.new_tag('p',**{'class':'caption'}) + if len(p.contents) > 1: #if we have more than just the tag + for i in reversed(p.contents): + if i.name != 'img': #if it is not an tag + caption.insert(0,i.extract()) + p.insert_after(caption) instance._content = soup.decode() diff --git a/assets/requirements.txt b/assets/requirements.txt new file mode 100755 index 0000000..7725d6a --- /dev/null +++ b/assets/requirements.txt @@ -0,0 +1,2 @@ +cssmin +webassets \ No newline at end of file diff --git a/assets/test_assets.py b/assets/test_assets.py index 6eed987..3001073 100755 --- a/assets/test_assets.py +++ b/assets/test_assets.py @@ -18,11 +18,11 @@ CUR_DIR = os.path.dirname(__file__) THEME_DIR = os.path.join(CUR_DIR, 'test_data') CSS_REF = open(os.path.join(THEME_DIR, 'static', 'css', 'style.min.css')).read() -CSS_HASH = hashlib.md5(CSS_REF).hexdigest()[0:8] +CSS_HASH = hashlib.md5(CSS_REF.encode()).hexdigest()[0:8] @unittest.skipUnless(module_exists('webassets'), "webassets isn't installed") -@skipIfNoExecutable(['scss', '-v']) +@skipIfNoExecutable(['sass', '-v']) @skipIfNoExecutable(['cssmin', '--version']) class TestWebAssets(unittest.TestCase): """Base class for testing webassets.""" diff --git a/dither/dither.py b/dither/dither.py index 837c566..8cfc745 100644 --- a/dither/dither.py +++ b/dither/dither.py @@ -53,7 +53,7 @@ def _image_path(pelican): def _out_path(pelican): return os.path.join(pelican.settings['OUTPUT_PATH'], - pelican.settings.get('DITHER_DIR', DEFAULT_DITHER_DIR)) + pelican.settings.get('DITHER_DIR', DEFAULT_DITHER_DIR)).rstrip('/') def dither(pelican): global enabled @@ -64,34 +64,35 @@ def dither(pelican): out_path = _out_path(pelican) transparency = pelican.settings.get("TRANSPARENCY",DEFAULT_TRANSPARENCY) - + STABLE_SITEURL = pelican.settings.get("STABLE_SITEURL") if not os.path.exists(out_path): os.mkdir(out_path) - for dirpath, _, filenames in os.walk(in_path): - for filename in filenames: - file_, ext = os.path.splitext(filename) - fn= os.path.join(dirpath,filename) - of = os.path.join(out_path, filename.replace(ext,'.png')) - if not os.path.exists(of) and imghdr.what(fn): - logging.debug("dither plugin: dithering {}".format(fn)) + if pelican.settings.get("SITEURL") == STABLE_SITEURL: + for dirpath, _, filenames in os.walk(in_path): + for filename in filenames: + file_, ext = os.path.splitext(filename) + fn= os.path.join(dirpath,filename) + of = os.path.join(out_path, filename.replace(ext,'.png')) + if not os.path.exists(of) and imghdr.what(fn): + logging.debug("Dither plugin: dithering {}".format(fn)) - img= Image.open(fn).convert('RGB') + img= Image.open(fn).convert('RGB') - resize = pelican.settings.get('RESIZE', DEFAULT_RESIZE_OUTPUT) + resize = pelican.settings.get('RESIZE', DEFAULT_RESIZE_OUTPUT) - if resize: - image_size = pelican.settings.get('SIZE', DEFAULT_MAX_SIZE) - img.thumbnail(image_size, Image.LANCZOS) - - palette = hitherdither.palette.Palette(pelican.settings.get('DITHER_PALETTE', DEFAULT_DITHER_PALETTE)) - - threshold = pelican.settings.get('THRESHOLD', DEFAULT_THRESHOLD) - - img_dithered = hitherdither.ordered.bayer.bayer_dithering(img, palette, threshold, order=8) #see hither dither documentation for different dithering algos + if resize: + image_size = pelican.settings.get('SIZE', DEFAULT_MAX_SIZE) + img.thumbnail(image_size, Image.LANCZOS) + + palette = hitherdither.palette.Palette(pelican.settings.get('DITHER_PALETTE', DEFAULT_DITHER_PALETTE)) + + threshold = pelican.settings.get('THRESHOLD', DEFAULT_THRESHOLD) + + img_dithered = hitherdither.ordered.bayer.bayer_dithering(img, palette, threshold, order=8) #see hither dither documentation for different dithering algos - img_dithered.save(of, optimize=True) - #logging.debug(calculate_savings(in_path,out_path)) + img_dithered.save(of, optimize=True) + #logging.debug(calculate_savings(in_path,out_path)) def parse_for_images(instance): #based on better_figures_and_images plugin by @dflock, @phrawzty,@if1live,@jar1karp,@dhalperi,@aqw,@adworacz diff --git a/neighbors/Readme.rst b/neighbors/Readme.rst index 97045b8..022b399 100755 --- a/neighbors/Readme.rst +++ b/neighbors/Readme.rst @@ -1,7 +1,7 @@ Neighbor Articles Plugin for Pelican ==================================== -This plugin adds ``next_article`` (newer) and ``prev_article`` (older) +This plugin adds ``next_article`` (newer) and ``prev_article`` (older) variables to the article's context. Also adds ``next_article_in_category`` and ``prev_article_in_category``. @@ -27,7 +27,7 @@ Usage {% endif %} - +

+ Usage with the Subcategory plugin --------------------------------- If you want to get the neigbors within a subcategory it's a little different. Since an article can belong to more than one subcategory, subcategories are -stored in a list. If you have an article with subcategories like +stored in a list. If you have an article with subcategories like ``Category/Foo/Bar`` it will belong to both subcategory Foo, and Foo/Bar. Subcategory neighbors are -added to an article as ``next_article_in_subcategory#`` and +added to an article as ``next_article_in_subcategory#`` and ``prev_article_in_subcategory#`` where ``#`` is the level of subcategory. So using -the example from above, subcategory1 will be Foo, and subcategory2 Foo/Bar. +the example from above, subcategory1 will be Foo, and subcategory2 Foo/Bar. Therefor the usage with subcategories is: .. code-block:: html+jinja @@ -77,7 +77,7 @@ Therefor the usage with subcategories is: {% endif %} - + + diff --git a/neighbors/neighbors.py b/neighbors/neighbors.py index b65fcfe..f76589f 100755 --- a/neighbors/neighbors.py +++ b/neighbors/neighbors.py @@ -3,19 +3,24 @@ Neighbor Articles Plugin for Pelican ==================================== -This plugin adds ``next_article`` (newer) and ``prev_article`` (older) +This plugin adds ``next_article`` (newer) and ``prev_article`` (older) variables to the article's context """ from pelican import signals + def iter3(seq): - it = iter(seq) - nxt = None - cur = next(it) - for prv in it: - yield nxt, cur, prv + """Generate one triplet per element in 'seq' following PEP-479.""" + nxt, cur = None, None + for prv in seq: + if cur: + yield nxt, cur, prv nxt, cur = cur, prv - yield nxt, cur, None + # Don't yield anything if empty seq + if cur: + # Yield last element in seq (also if len(seq) == 1) + yield nxt, cur, None + def get_translation(article, prefered_language): if not article: @@ -25,6 +30,7 @@ def get_translation(article, prefered_language): return translation return article + def set_neighbors(articles, next_name, prev_name): for nxt, cur, prv in iter3(articles): exec("cur.{} = nxt".format(next_name)) @@ -32,27 +38,29 @@ def set_neighbors(articles, next_name, prev_name): for translation in cur.translations: exec( - "translation.{} = get_translation(nxt, translation.lang)".format( - next_name)) + "translation.{} = get_translation(nxt, translation.lang)" + .format(next_name)) exec( - "translation.{} = get_translation(prv, translation.lang)".format( - prev_name)) - + "translation.{} = get_translation(prv, translation.lang)" + .format(prev_name)) + + def neighbors(generator): set_neighbors(generator.articles, 'next_article', 'prev_article') - + for category, articles in generator.categories: - articles.sort(key=(lambda x: x.date), reverse=(True)) + articles.sort(key=lambda x: x.date, reverse=True) set_neighbors( articles, 'next_article_in_category', 'prev_article_in_category') if hasattr(generator, 'subcategories'): for subcategory, articles in generator.subcategories: - articles.sort(key=(lambda x: x.date), reverse=(True)) + articles.sort(key=lambda x: x.date, reverse=True) index = subcategory.name.count('/') next_name = 'next_article_in_subcategory{}'.format(index) prev_name = 'prev_article_in_subcategory{}'.format(index) set_neighbors(articles, next_name, prev_name) + def register(): signals.article_generator_finalized.connect(neighbors) diff --git a/neighbors/test_data/article.md b/neighbors/test_data/article.md new file mode 100644 index 0000000..89b6980 --- /dev/null +++ b/neighbors/test_data/article.md @@ -0,0 +1,14 @@ +Title: Test md File +Category: test +Tags: foo, bar, foobar +Date: 2010-12-02 10:14 +Modified: 2010-12-02 10:20 +Summary: I have a lot to test + +Test Markdown File Header +========================= + +Used for pelican test +--------------------- + +The quick brown fox jumped over the lazy dog's back. diff --git a/neighbors/test_neighbors.py b/neighbors/test_neighbors.py new file mode 100644 index 0000000..f9865fa --- /dev/null +++ b/neighbors/test_neighbors.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from os.path import dirname, join +from tempfile import TemporaryDirectory + +from pelican.generators import ArticlesGenerator +from pelican.tests.support import get_settings, unittest + +from .neighbors import neighbors + + +CUR_DIR = dirname(__file__) + + +class NeighborsTest(unittest.TestCase): + def test_neighbors_basic(self): + with TemporaryDirectory() as tmpdirname: + generator = _build_article_generator(join(CUR_DIR, '..', 'test_data'), tmpdirname) + neighbors(generator) + def test_neighbors_with_single_article(self): + with TemporaryDirectory() as tmpdirname: + generator = _build_article_generator(join(CUR_DIR, 'test_data'), tmpdirname) + neighbors(generator) + + +def _build_article_generator(content_path, output_path): + settings = get_settings(filenames={}) + settings['PATH'] = content_path + context = settings.copy() + context['generated_content'] = dict() + context['static_links'] = set() + article_generator = ArticlesGenerator( + context=context, settings=settings, + path=settings['PATH'], theme=settings['THEME'], output_path=output_path) + article_generator.generate_context() + return article_generator diff --git a/page_metadata/README.md b/page_metadata/README.md new file mode 100644 index 0000000..0434e2d --- /dev/null +++ b/page_metadata/README.md @@ -0,0 +1,32 @@ +#Page Meta-Data + +A Pelican plugin to add the total page size to each generated page of your site. + +It calculates the weight of the HTML page including all image media and returns that in a human readable format (B, KB, MB). + +## Caveats: +it currently is tailored to https://solar.lowtechmagazine.com and needs work in the following areas: + +* add options to show file name and generation time +* properly handle subsites plugin (currently it only works for dither+subsites) +* make sure it works with --relative-urls flag +* handle static assets + + +## Use: +To enable the plugin add it to the `PLUGINS` list in `pelicanconf.py`. + +Add a div with id `page-size` to your template and `page_metadata` will place the result there. + +have fun! + + +## in case we add generation time: + +To use this plugin first import `strftime` at the top of `pelicanconf.py`: + +`from time import strftime` + +Then add `NOW = strftime('%c')` somewhere in that document as well. This saves the time of generation as a variable that is usable by the `page_metadata` plugin. + + diff --git a/page_metadata/__init__.py b/page_metadata/__init__.py new file mode 100644 index 0000000..0c815c3 --- /dev/null +++ b/page_metadata/__init__.py @@ -0,0 +1 @@ +from .page_metadata import * \ No newline at end of file diff --git a/page_metadata/page_metadata.py b/page_metadata/page_metadata.py new file mode 100644 index 0000000..91eb5cc --- /dev/null +++ b/page_metadata/page_metadata.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python + +# -*- coding: utf-8 -*- # + +# Page Meta-Data +# ------------------------ +# Insert meta-data about the generated file into the resulting HMTL. +# Copyright (C) 2019 Roel Roscam Abbing +# +# Support your local Low-Tech Magazine: +# https://solar.lowtechmagazine.com/donate.html + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import unicode_literals +from pelican import signals +from bs4 import BeautifulSoup +import os + + +def get_printable_size(byte_size): + """ + Thanks Pobux! + https://gist.github.com/Pobux/0c474672b3acd4473d459d3219675ad8 + """ + BASE_SIZE = 1024.00 + MEASURE = ["B", "KB", "MB", "GB", "TB", "PB"] + + def _fix_size(size, size_index): + if not size: + return "0" + elif size_index == 0: + return str(size) + else: + return "{:.2f}".format(size) + + current_size = byte_size + size_index = 0 + + while current_size >= BASE_SIZE and len(MEASURE) != size_index: + current_size = current_size / BASE_SIZE + size_index = size_index + 1 + + size = _fix_size(current_size, size_index) + measure = MEASURE[size_index] + return size + measure + +def get_assets(soup): + assets = [] + for a in soup.findAll('link', {'rel':['apple-touch-icon','icon','stylesheet']}): + a = a['href'].split('?')[0] + if a not in assets: + assets.append(a) + return assets + +def get_media(html_file): + """ + Currently only images because I, for one, am lazy. + """ + html_file = open(html_file).read() + soup = BeautifulSoup(html_file, 'html.parser') + media = [] + + for img in soup(['img', 'object']): + media.append(img['src']) + + featured_images = soup.findAll('div', {'class':'featured-img'}) + for fi in featured_images: + fi = fi['style'] + start = fi.find("url('") + end = fi.find("');") + url = fi[start+len("url('"):end] + media.append(url) + + assets = get_assets(soup) + media = list(set(media+assets)) # duplicate media don't increase page size + return media, soup + +def generate_metadata(path, context): + output_path = context['OUTPUT_PATH'] + output_file = context['output_file'] + siteurl = context['SITEURL'] + plugins = context['PLUGINS'] + subsites = False + + if 'i18n_subsites' in plugins: + subsites = True + lang = context['DEFAULT_LANG'] + general_output_path = output_path.replace(lang, '') + siteurl = context['main_siteurl'] + + media_size = 0 + # enumerate all media displayed on the page + media, soup = get_media(path) #reuse the same soup to limit calculation + + for m in media: + # filter out SITEURL to prevent trouble + # join output path to file, need to strip any leading slash for os.path + if subsites: + file_name = m.replace(context['main_siteurl']+'/', '') + m = os.path.join(general_output_path, file_name.strip('/')) + else: + file_name = m.replace(context['SITEURL']+'/', '') + m = os.path.join(output_path, file_name.strip('/')) + #print(m) + if os.path.exists(m): + #print(m, 'exists') + media_size = media_size + os.path.getsize(m) + + current_file = os.path.join(output_path, output_file) + file_size = os.path.getsize(current_file) + + file_size = file_size + media_size + metadata = get_printable_size(file_size) + metadata = get_printable_size(file_size+len(metadata)) # cursed code is cursed + + insert_metadata(path, metadata, soup) + +def insert_metadata(output_file, metadata, soup): + tag = soup.find('div', {'id':'page-size'}) + if tag: + with open(output_file,'w') as f: + tag.string = '{}'.format(metadata) + f.write(str(soup)) + +def register(): + signals.content_written.connect(generate_metadata) diff --git a/representative_image/Readme.md b/representative_image/Readme.md index 05c70fa..a6abbb4 100644 --- a/representative_image/Readme.md +++ b/representative_image/Readme.md @@ -1,29 +1,31 @@ # Summary -This plugin extracts a representative image (i.e, featured image) from the article's summary or content if not specifed in the metadata. +This plugin extracts a representative image (i.e, featured image) from the summary or content of an article or a page, if not specified in the metadata. -The plugin also removes any images from the summary after extraction to avoid duplication. +The plugin also removes any images from the summary after extraction to avoid duplication. -It allows the flexibility on where and how to display the featured image of an article together with its summary in a template page. For example, the article metadata can be displayed in thumbnail format, in which there is a short summary and an image. The layout of the summary and the image can be varied for aesthetical purpose. It doesn't have to depend on article's content format. +It allows the flexibility on where and how to display the featured image of an article together with its summary in a template page. For example, the article metadata can be displayed in thumbnail format, in which there is a short summary and an image. The layout of the summary and the image can be varied for aesthetical purpose. It doesn't have to depend on article's content format. -Installation ------------- +## Installation -This plugin requires BeautifulSoup. +This plugin requires `BeautifulSoup`: - pip install beautifulsoup4 +```bash +pip install beautifulsoup4 +``` -To enable, add the following to your settings.py: +To enable, add the following to your `settings.py`: - PLUGIN_PATH = 'path/to/pelican-plugins' - PLUGINS = ["representative_image"] +```python +PLUGIN_PATH = 'path/to/pelican-plugins' +PLUGINS = ["representative_image"] +``` `PLUGIN_PATH` can be a path relative to your settings file or an absolute path. -Usage ------ +## Usage -To override the default behaviour of selecting the first image in the article's summary or content, set the image property the article's metadata to the url of the image to display, e.g: +To override the default behavior of selecting the first image in the article's summary or content, set the image property the article's metadata to the URL of the image to display, e.g: ```markdown Title: My super title @@ -38,8 +40,22 @@ Image: /images/my-super-image.png Article content... ``` +### Page + To include a representative image in a page add the following to the template: - {% if article.featured_image %} - - {% endif %} +```html +{% if page.featured_image %} + +{% endif %} +``` + +### Article + +To include a representative image in an article add the following to the template: + +```html +{% if article.featured_image %} + +{% endif %} +``` diff --git a/representative_image/__init__.py b/representative_image/__init__.py index f52720f..88c52b4 100644 --- a/representative_image/__init__.py +++ b/representative_image/__init__.py @@ -1 +1 @@ -from .representative_image import * +from .representative_image import * # noqa diff --git a/representative_image/representative_image.py b/representative_image/representative_image.py index 64d68ce..2cec1a2 100644 --- a/representative_image/representative_image.py +++ b/representative_image/representative_image.py @@ -1,8 +1,9 @@ import six +from bs4 import BeautifulSoup + from pelican import signals from pelican.contents import Article, Page -from pelican.generators import ArticlesGenerator -from bs4 import BeautifulSoup +from pelican.generators import ArticlesGenerator, PagesGenerator def images_extraction(instance): @@ -12,7 +13,8 @@ def images_extraction(instance): representativeImage = instance.metadata['image'] # Process Summary: - # If summary contains images, extract one to be the representativeImage and remove images from summary + # If summary contains images, extract one to be the representativeImage + # and remove images from summary soup = BeautifulSoup(instance.summary, 'html.parser') images = soup.find_all('img') for i in images: @@ -20,7 +22,8 @@ def images_extraction(instance): representativeImage = i['src'] i.extract() if len(images) > 0: - # set _summary field which is based on metadata. summary field is only based on article's content and not settable + # set _summary field which is based on metadata. summary field is + # only based on article's content and not settable instance._summary = six.text_type(soup) # If there are no image in summary, look for it in the content body @@ -32,6 +35,9 @@ def images_extraction(instance): # Set the attribute to content instance instance.featured_image = representativeImage + instance.featured_alt = instance.metadata.get('alt', None) + instance.featured_link = instance.metadata.get('link', None) + instance.featured_caption = instance.metadata.get('caption', None) def run_plugin(generators): @@ -39,6 +45,13 @@ def run_plugin(generators): if isinstance(generator, ArticlesGenerator): for article in generator.articles: images_extraction(article) + for translation in article.translations: + images_extraction(translation) + elif isinstance(generator, PagesGenerator): + for page in generator.pages: + images_extraction(page) + for translation in page.translations: + images_extraction(translation) def register(): diff --git a/representative_image/requirements.txt b/representative_image/requirements.txt new file mode 100644 index 0000000..c1f5f71 --- /dev/null +++ b/representative_image/requirements.txt @@ -0,0 +1 @@ +beautifulsoup4 diff --git a/representative_image/test_representative_image.py b/representative_image/test_representative_image.py new file mode 100755 index 0000000..949c35e --- /dev/null +++ b/representative_image/test_representative_image.py @@ -0,0 +1,73 @@ +#!/bin/sh +import unittest + +import representative_image +from jinja2.utils import generate_lorem_ipsum +from pelican.contents import Article, Page + +# Generate content with image +TEST_CONTENT_IMAGE_URL = 'https://testimage.com/test.jpg' +TEST_CONTENT = str(generate_lorem_ipsum(n=3, html=True)) + ''+ str(generate_lorem_ipsum(n=2,html=True)) # noqa +TEST_SUMMARY_IMAGE_URL = 'https://testimage.com/summary.jpg' +TEST_SUMMARY_WITHOUTIMAGE = str(generate_lorem_ipsum(n=1, html=True)) +TEST_SUMMARY_WITHIMAGE = TEST_SUMMARY_WITHOUTIMAGE + '' # noqa +TEST_CUSTOM_IMAGE_URL = 'https://testimage.com/custom.jpg' + + +class TestRepresentativeImage(unittest.TestCase): + + def setUp(self): + super(TestRepresentativeImage, self).setUp() + representative_image.register() + + def test_extract_image_from_content(self): + args = { + 'content': TEST_CONTENT, + 'metadata': { + 'summary': TEST_SUMMARY_WITHOUTIMAGE, + }, + } + + article = Article(**args) + self.assertEqual(article.featured_image, TEST_CONTENT_IMAGE_URL) + + def test_extract_image_from_summary(self): + args = { + 'content': TEST_CONTENT, + 'metadata': { + 'summary': TEST_SUMMARY_WITHIMAGE, + }, + } + + article = Article(**args) + self.assertEqual(article.featured_image, TEST_SUMMARY_IMAGE_URL) + self.assertEqual(article.summary, TEST_SUMMARY_WITHOUTIMAGE) + + def test_extract_image_from_summary_with_custom_image(self): + args = { + 'content': TEST_CONTENT, + 'metadata': { + 'summary': TEST_SUMMARY_WITHIMAGE, + 'image': TEST_CUSTOM_IMAGE_URL, + }, + } + + article = Article(**args) + self.assertEqual(article.featured_image, TEST_CUSTOM_IMAGE_URL) + self.assertEqual(article.summary, TEST_SUMMARY_WITHOUTIMAGE) + + def test_extract_image_from_page_summary_with_custom_image(self): + args = { + 'content': TEST_CONTENT, + 'metadata': { + 'summary': TEST_SUMMARY_WITHIMAGE, + 'image': TEST_CUSTOM_IMAGE_URL, + }, + } + page = Page(**args) + self.assertEqual(page.featured_image, TEST_CUSTOM_IMAGE_URL) + self.assertEqual(page.summary, TEST_SUMMARY_WITHOUTIMAGE) + + +if __name__ == '__main__': + unittest.main()