Commit 59758e5f by Xavier Antoviaque

Merge pull request #1 from open-craft/first-version

(WIP) First Version.
parents fe27bd5f aee05410
# Created by https://www.gitignore.io
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
/*.iml
## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:
# User-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle:
# .idea/gradle.xml
# .idea/libraries
# Mongo Explorer plugin:
# .idea/mongoSettings.xml
## File-based project format:
*.ipr
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
### vim ###
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
*~
### Linux ###
*~
# KDE directory preferences
.directory
from .poll import PollBlock
\ No newline at end of file
This diff is collapsed. Click to expand it.
/* CSS for PollBlock Student View */
.poll-answer {
margin-left: 1em;
font-weight: bold;
display: inline-block;
}
.percentage-gauge {
display: inline-block;
background-color: #e5ebee;
position: relative;
z-index: 1;
}
.poll-result-input-container {
display: table-cell;
vertical-align: middle;
}
.poll-result-input-container input {
margin: 0 .5em 0 0;
}
.percentage-gauge-container {
display: table-cell;
width: 100%;
vertical-align: middle;
background-color: #fafbfc;
}
ul.poll-answers, ul.poll-answers-results {
list-style-type: none !important;
max-width: 80%;
}
li.poll-answer {
display: block;
border-bottom-width: .5em;
vertical-align: middle;
margin-top: 5px;
margin-bottom: 5px;
}
li.poll-spacer {
height: .25em;
}
ul.poll-answers-results {
display: table;
}
li.poll-result {
width: 100%;
display: table-row;
padding-bottom: .2em;
}
.poll-answer-label {
margin-left: .2em;
font-weight: bold;
display: inline-block;
margin-bottom: 5px;
margin-top: 5px;
}
.poll-image {
width: 25%;
display: inline-block;
vertical-align: middle;
}
.poll-image {
margin-left: .5em;
}
li.poll-result .poll-image {
display: table-cell;
margin-left: 0;
}
.poll-image img{
width: 100%;
}
.poll-percent-container {
display: table-cell;
text-align: left;
padding-left: .2em;
vertical-align: middle;
}
.poll-percent-display {
font-weight: bold;
}
.poll-top-choice {
color: #e37222;
}
.poll-footnote {
margin-top: 1em;
margin-bottom: 1em;
font-size: smaller;
}
\ No newline at end of file
/* CSS for PollBlock Studio Menu View */
.poll-delete-answer {
float: right;
margin-top: 1em;
}
#poll-question-editor-container, #poll-feedback-editor-container{
width: 100%;
text-align: center;
}
#poll-question-editor, #poll-feedback-editor{
width: 98%;
height: 7em;
text-align: left;
color: #4C4C4C;
margin-top: 0.5em;
margin-bottom: 0.5em;
box-shadow: 0 0 9px #555 inset;
border: 1px solid #B2B2B2;
border-radius: 3px;
padding: 10px;
}
.poll-move-up {
opacity: .5;
}
.poll-move-down {
opacity: .5;
}
.poll-move-down:hover, .poll-move-up:hover {
opacity: 1;
transition: opacity .4s;
cursor: pointer
}
.poll-move {
float: right;
}
\ No newline at end of file
<script id="results" type="text/html">
{{{question}}}
<ul class="poll-answers-results">
{{#each tally}}
<li class="poll-result">
<div class="poll-result-input-container">
<input id="answer-{{key}}" type="radio" disabled {{#if choice}}checked="True"{{/if}} />
</div>
{{#if any_img}}
<div class="poll-image result-image">
<label for="answer-{{key}}" class="poll-image-label">
{{#if img}}
<img src="{{img}}" />
{{/if}}
</label>
</div>
{{/if}}
<div class="percentage-gauge-container">
<div class="percentage-gauge" style="width:{{percent}}%;">
<label class="poll-answer-label" for="answer-{{key}}">{{answer}}</label>
</div>
</div>
<div class="poll-percent-container">
<span class="poll-percent-display{{#if top}} poll-top-choice{{/if}}">{{percent}}%</span>
</div>
</li>
{{^last}}
<li class="poll-spacer">
</li>
{{/last}}
{{/each}}
</ul>
<input class="input-main" type="button" name="poll-submit" value="Submit" disabled>
<div class="poll-footnote">Results gathered from {{total}} respondent(s).</div>
{{#if feedback}}
<hr />
<div class="poll-feedback">
{{{feedback}}}
</div>
{{/if}}
</script>
\ No newline at end of file
<script id="answer-form-component" type="text/html">
{{#each answers}}
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="answer-{{key}}">Answer</label>
<input class="input setting-input" name="answer-{{key}}" id="answer-{{key}}" value="{{text}}" type="text" /><br />
<label class="label setting-label" for="img-answer-{{key}}">Image URL</label>
<input class="input setting-input" name="img-answer-{{key}}" id="img-answer-{{key}}" value="{{img}}" type="text" />
<div class="poll-move">
<div class="poll-move-up">&#9650;</div>
<div class="poll-move-down">&#9660;</div>
</div>
</div>
<span class="tip setting-help">
Enter an answer for the user to select. An answer must have an image URL or text, and can have both.
(Text truncated at 250 characters, Image URL at 1000)
</span>
<a href="#" class="button action-button poll-delete-answer" onclick="return false;">Delete</a>
</li>
{{/each}}
</script>
\ No newline at end of file
{{ js_template|safe }}
<div class="poll-block">
{# If no form is present, the Javascript will load the results instead. #}
{% if not choice %}
<form>
<div class="poll-question-container">
{{question|safe}}
</div>
<ul class="poll-answers">
{% for key, value in answers %}
<li class="poll-answer">
<input type="radio" name="choice" id="{{url_name}}-answer-{{key}}" value="{{key}}" />
{% if value.img %}
<div class="poll-image">
<label for="{{url_name}}-answer-{{key}}" class="poll-image-label">
{% if value.img %}
<img src="{{value.img}}" />
{% endif %}
</label>
</div>
{% endif %}
<label class="poll-answer" for="{{url_name}}-answer-{{key}}">{{value.label}}</label>
</li>
{% endfor %}
</ul>
<input class="input-main" type="button" name="poll-submit" value="Submit" disabled />
</form>
{% endif %}
</div>
\ No newline at end of file
{{js_template|safe}}
<div class="wrapper-comp-settings is-active editor-with-buttons" id="settings-tab">
<form id="poll-form">
<ul class="list-input settings-list" id="poll-line-items">
<li class="field comp-setting-entry is-set">
<h2><label for="poll-question-editor">Question/Prompt</label></h2>
<a href="//daringfireball.net/projects/markdown/syntax" target="_blank">Markdown Syntax</a> is supported.
<div id="poll-question-editor-container">
<textarea class="input setting-input" name="question" id="poll-question-editor">{{question}}</textarea>
</div>
<span class="tip setting-help">Enter the prompt for the user. (Truncated after 5000 characters)</span>
</li>
<li class="field comp-setting-entry is-set">
<h2><label for="poll-feedback-editor">Feedback</label></h2>
<a href="//daringfireball.net/projects/markdown/syntax" target="_blank">Markdown Syntax</a> is supported.
<div id="poll-feedback-editor-container">
<textarea class="input setting-input" name="feedback" id="poll-feedback-editor">{{feedback}}</textarea>
</div>
<span class="tip setting-help">
This text will be displayed for the user as some extra feedback after they have
submitted their response to the poll. (Truncated after 5000 characters)
</span>
</li>
<li class="field comp-setting-entry is-set">
<p>
<strong>Notes:</strong>
If you change an answer's text, all students who voted for that choice will have their votes updated to
the new text. You'll want to avoid changing an answer from something like 'True' to 'False', accordingly.
If you delete an answer, any votes for that answer will also be deleted. Students whose choices are deleted
may vote again, but will not lose course progress.
</p>
</li>
</ul>
<div class="xblock-actions">
<ul>
<li class="action-item" id="poll-add-answer">
<a href="#" class="button action-button" class="poll-add-answer-link" onclick="return false;">Add Answer</a>
</li>
<li class="action-item">
<input id="poll-submit-options" type="submit" class="button action-primary save-button" value="Save" onclick="return false;" />
</li>
<li class="action-item">
<a href="#" class="button cancel-button">Cancel</a>
</li>
</ul>
</div>
</form>
</div>
\ No newline at end of file
/* Javascript for PollBlock. */
function PollBlock(runtime, element) {
var voteUrl = runtime.handlerUrl(element, 'vote');
var tallyURL = runtime.handlerUrl(element, 'get_results');
var submit = $('input[type=button]', element);
var resultsTemplate = Handlebars.compile($("#results", element).html());
function getResults(data) {
if (! data['success']) {
alert(data['errors'].join('\n'));
}
$.ajax({
// Semantically, this would be better as GET, but we can use helper
// functions with POST.
type: "POST",
url: tallyURL,
data: JSON.stringify({}),
success: function (data) {
$('div.poll-block', element).html(resultsTemplate(data));
}
})
}
function enableSubmit() {
submit.removeAttr("disabled");
answers.unbind("change.EnableSubmit");
}
// If the submit button doesn't exist, the user has already
// selected a choice.
if (submit.length) {
var radio = $('input[name=choice]:checked', element);
submit.click(function (event) {
// Refresh.
radio = $(radio.selector, element);
var choice = radio.val();
$.ajax({
type: "POST",
url: voteUrl,
data: JSON.stringify({"choice": choice}),
success: getResults
});
});
// If the user has refreshed the page, they may still have an answer
// selected and the submit button should be enabled.
var answers = $('input[type=radio]', element);
if (! radio.val()) {
answers.bind("change.EnableSubmit", enableSubmit);
} else {
enableSubmit();
}
} else {
getResults({'success': true});
}
}
\ No newline at end of file
function PollEditBlock(runtime, element) {
var loadAnswers = runtime.handlerUrl(element, 'load_answers');
var temp = $('#answer-form-component', element).html();
var answerTemplate = Handlebars.compile(temp);
var pollLineItems =$('#poll-line-items', element);
function empowerDeletes(scope) {
$('.poll-delete-answer', scope).click(function () {
$(this).parent().remove();
});
}
/*
The poll answers need to be reorderable. As the UL they are in is not
easily isolated, we need to start checking their position to make
sure they aren't ordered above the other settings, which are also
in the list.
*/
var starting_point = 3;
function empowerArrows(scope) {
$('.poll-move-up', scope).click(function () {
var tag = $(this).parents('li');
if (tag.index() <= starting_point){
return;
}
tag.prev().before(tag);
tag.fadeOut("fast", "swing").fadeIn("fast", "swing");
});
$('.poll-move-down', scope).click(function () {
var tag = $(this).parents('li');
if ((tag.index() >= (tag.parent().children().length - 1))) {
return;
}
tag.next().after(tag);
tag.fadeOut("fast", "swing").fadeIn("fast", "swing");
});
}
function displayAnswers(data) {
pollLineItems.append(answerTemplate(data));
empowerDeletes(element);
empowerArrows(element);
}
$('#poll-add-answer', element).click(function () {
// The degree of precision on date should be precise enough to avoid
// collisions in the real world.
pollLineItems.append(answerTemplate({'answers': [{'key': new Date().getTime(), 'text': ''}]}));
var new_answer = $(pollLineItems.children().last());
empowerDeletes(new_answer);
empowerArrows(new_answer);
new_answer.fadeOut(250).fadeIn(250);
});
$(element).find('.cancel-button', element).bind('click', function() {
runtime.notify('cancel', {});
});
$(element).find('.save-button', element).bind('click', function() {
var handlerUrl = runtime.handlerUrl(element, 'studio_submit');
var data = {'answers': []};
var tracker = [];
$('#poll-form input', element).each(function(i) {
var key = 'label';
if (this.name.indexOf('answer-') >= 0){
var name = this.name.replace('answer-', '');
if (this.name.indexOf('img-') == 0){
name = name.replace('img-', '');
key = 'img'
}
if (tracker.indexOf(name) == -1){
tracker.push(name);
data['answers'].push({'key': name})
}
var index = tracker.indexOf(name);
data['answers'][index][key] = this.value;
return
}
data[this.name] = this.value
});
data['title'] = $('#poll-title', element).val();
data['question'] = $('#poll-question-editor', element).val();
data['feedback'] = $('#poll-feedback-editor', element).val();
function check_return(data) {
if (data['success']) {
window.location.reload(false);
return;
}
alert(data['errors'].join('\n'));
}
$.ajax({
type: "POST",
url: handlerUrl,
data: JSON.stringify(data),
success: check_return
});
});
$(function ($) {
$.ajax({
type: "POST",
url: loadAnswers,
data: JSON.stringify({}),
success: displayAnswers
});
});
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
import bleach
from markdown import markdown
ALLOWED_TAGS = {
'h1': [], 'h2': [], 'h3': [], 'h4': [], 'h5': [], 'h6': [],
'a': ['target', 'href', 'class'], 'strong': [], 'em': [], 'blockquote': [],
'pre': [], 'li': [], 'ul': [], 'ol': [], 'code': ['class'], 'p': [],
}
def process_markdown(raw_text):
return bleach.clean(markdown(raw_text), tags=ALLOWED_TAGS, strip_comments=False)
\ No newline at end of file
-e .
bleach
markdown
-e git+https://github.com/edx-solutions/xblock-utils.git#egg=xblock-utils
\ No newline at end of file
"""Setup for poll XBlock."""
import os
from setuptools import setup
def package_data(pkg, roots):
"""Generic function to find package_data.
All of the files under each of the `roots` will be declared as package
data for package `pkg`.
"""
data = []
for root in roots:
for dirname, _, files in os.walk(os.path.join(pkg, root)):
for fname in files:
data.append(os.path.relpath(os.path.join(dirname, fname), pkg))
return {pkg: data}
setup(
name='xblock-poll',
version='0.1',
description='An XBlock for polling users.',
packages=[
'poll',
],
install_requires=[
'XBlock',
'markdown',
'bleach',
'xblock-utils',
],
dependency_links=['http://github.com/edx-solutions/xblock-utils/tarball/master#egg=xblock-utils'],
entry_points={
'xblock.v1': [
'poll = poll:PollBlock',
]
},
package_data=package_data("poll", ["static", "public"]),
)
\ No newline at end of file
from xblockutils.base_test import SeleniumBaseTest
class PollBaseTest(SeleniumBaseTest):
default_css_selector = 'div.poll-block'
module_name = __name__
def get_submit(self):
return self.browser.find_element_by_css_selector('input[name="poll-submit"]')
\ No newline at end of file
"""
Tests to verify a default poll XBlock is a functional demo.
Deeper investigation should be tested in test_poll_functions.
"""
from selenium.common.exceptions import NoSuchElementException
from .base_test import PollBaseTest
class TestDefaults(PollBaseTest):
"""
Tests to run against the default poll.
"""
def test_default_poll(self):
"""
Verifies that a default poll loads, that it can be voted on, and that
the tally displays afterward. Verifies that the feedback section does
not load since it is not enabled by default.
"""
self.go_to_page('Defaults')
button = self.browser.find_element_by_css_selector('input[type=radio]')
button.click()
submit = self.get_submit()
submit.click()
self.wait_until_exists('.poll-percent-display')
# Should now be on the results page.
self.assertEqual(self.browser.find_element_by_css_selector('.poll-percent-display').text, '100%')
# No feedback section.
self.assertRaises(NoSuchElementException, self.browser.find_element_by_css_selector, '.poll-feedback')
\ No newline at end of file
"""
Test to make sure the layout for results is sane when taking images into
account.
"""
from tests.integration.base_test import PollBaseTest
import time
class TestLayout(PollBaseTest):
"""
Do tests to verify that the layout of elements makes sense depeneding on
the number of images.
"""
def test_all_images(self):
"""
Verify img tags are created for answers when they're all set.
"""
self.go_to_page('All Pictures')
pics = self.browser.find_elements_by_css_selector('.poll-image')
self.assertEqual(len(pics), 4)
# Pics should be within labels.
pics[0].find_element_by_css_selector('img').click()
self.get_submit().click()
self.wait_until_exists('.poll-image')
self.assertEqual(len(self.browser.find_elements_by_css_selector('.poll-image')), 4)
def test_one_image(self):
"""
Verify layout is sane when only one answer has an image.
"""
self.go_to_page('One Picture')
pics = self.browser.find_elements_by_css_selector('.poll-image')
# On the polling page, there should only be one pics div.
self.assertEqual(len(pics), 1)
pics[0].find_element_by_css_selector('img').click()
self.get_submit().click()
self.wait_until_exists('.poll-image.result-image')
# ...But on the results page, we need four, for table layout.
self.assertEqual(len(self.browser.find_elements_by_css_selector('.poll-image')), 4)
\ No newline at end of file
"""
Tests to make sure that markdown is both useful and secure.
"""
from .base_test import PollBaseTest
class MarkdownTestCase(PollBaseTest):
"""
Tests for the Markdown functionality.
"""
def test_question_markdown(self):
"""
Ensure Markdown is parsed for questions.
"""
self.go_to_page("Markdown")
self.assertEqual(
self.browser.find_element_by_css_selector('.poll-question-container').text,
"""This is a test
This is only a ><test
One
Two
Three
First
Second
Third
We shall find out if markdown is respected.
"I have not yet begun to code.\"""")
def test_feedback_markdown(self):
"""
Ensure Markdown is parsed for feedback.
"""
self.go_to_page("Markdown")
self.browser.find_element_by_css_selector('input[type=radio]').click()
self.get_submit().click()
self.wait_until_exists('.poll-feedback')
self.assertEqual(
self.browser.find_element_by_css_selector('.poll-feedback').text,
"""This is some feedback
This is a link
This is also a link.
This is a paragraph with emphasized and bold text, and both.""")
\ No newline at end of file
"""
Tests a realistic, configured Poll to make sure that everything works as it
should.
"""
from selenium.common.exceptions import NoSuchElementException
import time
from .base_test import PollBaseTest
class TestPollFunctions(PollBaseTest):
def test_first_load(self):
"""
Checks first load.
Verify that the poll loads with the expected choices, that feedback is
not showing, and that the submit button is disabled.
"""
self.go_to_page('Poll Functions')
answer_elements = self.browser.find_elements_by_css_selector('label.poll-answer')
answers = [element.text for element in answer_elements]
self.assertEqual(['A very long time', 'Not very long', 'I shall not say', 'Longer than you'], answers)
self.assertRaises(NoSuchElementException, self.browser.find_element_by_css_selector, '.poll-feedback')
submit_button = self.get_submit()
self.assertFalse(submit_button.is_enabled())
def test_submit_enabled(self):
"""
Makes sure the submit button is enabled when selecting an answer.
"""
self.go_to_page('Poll Functions')
answer_elements = self.browser.find_elements_by_css_selector('label.poll-answer')
answer_elements[0].click()
# When an answer is selected, make sure submit is enabled.
self.wait_until_exists('input[name=poll-submit]:enabled')
def test_poll_submission(self):
"""
Verify that the user can submit his or her vote and that the vote counts.
Also check that feedback is displayed afterward.
"""
self.go_to_page('Poll Functions')
answer_elements = self.browser.find_elements_by_css_selector('label.poll-answer')
# 'Not very long'
answer_elements[1].click()
self.get_submit().click()
# Not a good way to wait here, since all the elements we care about
# tracking don't exist yet.
time.sleep(1)
self.assertTrue(self.browser.find_element_by_css_selector('.poll-feedback').text,
"Thank you\nfor being a valued student.")
self.assertEqual(self.browser.find_element_by_css_selector('.poll-footnote').text,
'Results gathered from 100 respondent(s).')
self.assertFalse(self.browser.find_element_by_css_selector('input[name=poll-submit]').is_enabled())
def test_submit_not_enabled_on_revisit(self):
"""
Verify that revisiting the page post-vote does not re-enable the submit button.
"""
self.go_to_page('Poll Functions')
answer_elements = self.browser.find_elements_by_css_selector('label.poll-answer')
# Not very long
answer_elements[1].click()
self.get_submit().click()
# Button will be reaplaced with a new disabled copy, not just disabled.
self.wait_until_exists('input[name=poll-submit]:disabled')
self.go_to_page('Poll Functions')
self.assertFalse(self.get_submit().is_enabled())
\ No newline at end of file
<vertical_demo>
<poll tally="{'red': 20, 'fennec': 29, 'kit': 15, 'arctic' : 35}"
question="## What is your favorite kind of fox?"
answers="[['red', {'label': 'Red Fox', 'img': '../img/red_fox.png'}], ['fennec', {'label': 'Fennec Fox', 'img': '../img/fennec_fox.png'}], ['kit', {'label': 'Kit Fox', 'img': '../img/kit_fox.png'}], ['arctic', {'label': 'Arctic fox', 'img': '../img/arctic_fox.png'}]]" />
</vertical_demo>
\ No newline at end of file
<vertical_demo>
<poll url_name="defaults"/>
</vertical_demo>
\ No newline at end of file
<vertical_demo>
<poll url_name="markdown" question="## This is a test&#10;&#10;&lt;h1&gt;This is only a &amp;gt;&amp;lt;test&lt;/h1&gt;&#10;&#10;* One&#10;* Two&#10;* Three&#10;&#10;1. First&#10;2. Second&#10;3. Third&#10;&#10;We shall find out if markdown is respected.&#10;&#10;&gt; &quot;I have not yet begun to code.&quot;" feedback="### This is some feedback&#10;&#10;[This is a link](http://www.example.com)&#10;&#10;&lt;a href=&quot;http://www.example.com&quot; target=&quot;_blank&quot;&gt;This is also a link.&lt;/a&gt;&#10;&#10;This is a paragraph with *emphasized* and **bold** text, and **_both_**." />
</vertical_demo>
\ No newline at end of file
<vertical_demo>
<poll tally="{'red': 20, 'fennec': 29, 'kit': 15, 'arctic' : 35}"
question="## What is your favorite kind of fox?"
answers="[['red', {'label': 'Red Fox', 'img': None}], ['fennec', {'label': 'Fennec Fox', 'img': '../img/fennec_fox.png'}], ['kit', {'label': 'Kit Fox', 'img': None}], ['arctic', {'label': 'Arctic fox', 'img': None}]]" />
</vertical_demo>
\ No newline at end of file
<vertical_demo>
<poll tally="{'long': 20, 'short': 29, 'not_saying': 15, 'longer' : 35}"
question="## How long have you been studying with us?"
answers="[['long', {'label': 'A very long time', 'img': None}], ['short', {'label': 'Not very long', 'img': None}], ['not_saying', {'label': 'I shall not say', 'img': None}], ['longer', {'label': 'Longer than you', 'img': None}]]"
feedback="### Thank you&#10;&#10;for being a valued student."/>
</vertical_demo>
\ No newline at end of file
from unittest import TestCase
from poll.utils import process_markdown
class ProcessMarkdownTest(TestCase):
def test_markdown_escapes(self):
start_string = (
"""
## This is an H2.
<h3>This is an H3</h3>
<a href="http://example.com">This link should be preserved.</a>
[This link should be converted.](http://www.example.com)
This is a paragraph of text, despite being just one sentence.
&lt; This should be a less-than symbol.
< So should this, since it's not attached to anything.
> This is going to be a blockquote.
<script type="text/javascript">breakstuff();</script>
"""
)
end_string = (
"""<h2>This is an H2.</h2>
<h3>This is an H3</h3>
<p><a href="http://example.com">This link should be preserved.</a>
<a href="http://www.example.com">This link should be converted.</a></p>
<p>This is a paragraph of text, despite being just one sentence.</p>
<p>&lt; This should be a less-than symbol.</p>
<p>&lt; So should this, since it's not attached to anything.</p>
<blockquote>
<p>This is going to be a blockquote.</p>
</blockquote>
&lt;script type="text/javascript"&gt;breakstuff();&lt;/script&gt;"""
)
self.assertEqual(end_string, process_markdown(start_string))
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment