self.client = client
def request(self, url, method, exp_status=200, params={}):
+ if not url.startswith('/quotes/'):
+ url = '/quotes/' + url
+
if method == 'get':
- response = self.client.get('/quotes/' + url)
+ response = self.client.get(url)
elif method == 'post':
- response = self.client.post('/quotes/' + url, params)
+ response = self.client.post(url, params)
else:
raise RuntimeError('Unknown method %s for %s' % (method, url))
assert response.status_code == exp_status
--- /dev/null
+import re
+from django.db import DatabaseError, transaction
+
+from quotes.models import QuoteTag, Quote, Work, Author
+
+def get_or_create_author(authorname, resultcontext):
+ authorname = authorname.strip()
+ try:
+ return Author.objects.get(name=authorname)
+ except Author.DoesNotExist:
+ a = Author.objects.create(name=authorname,
+ pvt_notes="<p>Created during mass import</p>")
+ resultcontext["created_authors"] += [a]
+ return a
+
+def get_or_create_work(workname, author, resultcontext):
+ workname = workname.strip()
+ try:
+ return Work.objects.get(name=workname, author=author)
+ except Work.DoesNotExist:
+ w = Work.objects.create(name=workname,
+ author=author,
+ pvt_notes="<p>Created during mass import</p>")
+ resultcontext["created_works"] += [w]
+ return w
+
+def get_or_create_tag(tagname, resultcontext):
+ try:
+ return QuoteTag.objects.get(tag=tagname)
+ except QuoteTag.DoesNotExist:
+ t = QuoteTag.objects.create(tag=tagname)
+ resultcontext["created_tags"] += [t]
+ return t
+
+def add_tags_on_quote(quote, tagline, resultcontext):
+ for tagname in tagline.split(","):
+ tagname = tagname.strip()
+ if not tagname:
+ continue
+ tag = get_or_create_tag(tagname, resultcontext)
+ quote.tags.add(tag)
+
+def paragraphize(text):
+ paragraph = ""
+ for line in text.splitlines():
+ line = line.strip()
+ if not line: continue
+ paragraph += "<p>%s</p>" % line
+ # rest of the HTML will be bleach.clean()'d
+ return paragraph
+
+def create_quote(quotetext, authorname, workname, tagline, resultcontext):
+ author = get_or_create_author(authorname, resultcontext)
+ work = get_or_create_work(workname, author, resultcontext)
+ quotetext = paragraphize(quotetext)
+ quote = Quote.objects.create(text=quotetext, work=work)
+ add_tags_on_quote(quote, tagline, resultcontext)
+ resultcontext["created_quotes"] += [quote]
+
+def create_all_quotes(allquotes, resultcontext):
+ quotesep = re.compile(r'^\s*===+.*$', re.MULTILINE)
+ elemsep = re.compile(r'^\s*---+.*$', re.MULTILINE)
+
+ for fullquote in quotesep.split(allquotes):
+ if not fullquote: continue
+
+ elements = elemsep.split(fullquote)
+ if len(elements) not in [3, 4]:
+ resultcontext["rejected"] += [fullquote]
+ continue
+ quotetext, authorname, workname = elements[0:3]
+ if len(elements) == 4:
+ tagline = elements[3]
+ else:
+ tagline = ''
+ create_quote(quotetext, authorname, workname, tagline,
+ resultcontext)
+
+def domassimport(allquotes):
+ resultcontext = {}
+ resultcontext["created_quotes"] = []
+ resultcontext["created_tags"] = []
+ resultcontext["created_works"] = []
+ resultcontext["created_authors"] = []
+ resultcontext["rejected"] = []
+ resultcontext["fatal_error"] = False
+
+ try:
+ with transaction.atomic():
+ create_all_quotes(allquotes, resultcontext)
+ except DatabaseError as e:
+ resultcontext["fatal_error"] = True
+ resultcontext["fatal_error_message"] = e.__cause__
+
+ return resultcontext
text-align: right;
}
-.quote .author .work_name {
+.work_name {
font-style: italic;
}
{% if not skip_author_notes %}
{% include "quotes/author_notes.html" with author=quote.work.author %}
{% endif %}
- {% if quote.tags.all %}
+ {% if quote.tags.all.count %}
<p class="tags">
Tags:
{% for tag in quote.tags.all %}
--- /dev/null
+{% extends 'quotes/base.html' %}
+
+{% block title %}Searching for quotes{% endblock %}
+
+{% block body %}
+
+{% if fatal_error %}
+ <p>Fatal error during import process. Transaction rolled back.</p>
+
+ <p>The error from the database was:</p>
+
+ <pre>{{ fatal_error_message }}</pre>
+{% endif %}
+
+{% if rejected %}
+ <p>Some quotes were rejected during import:</p>
+ {% for reject in rejected %}
+ <p>The quote made up of:</p>
+ <pre class="rejected">{{ reject }}</pre>
+ {% endfor %}
+{% endif %}
+
+<p>Created authors:</p>
+<ul>
+ {% for author in created_authors %}
+ <li><a href="{{ author.get_absolute_url }}"
+ class="author_name">{{ author.name }}</a></li>
+ {% empty %}
+ <li>No new author</li>
+ {% endfor %}
+</ul>
+
+<p>Created works:</p>
+<ul>
+ {% for work in created_works %}
+ <li><a href="{{ work.get_absolute_url }}"
+ class="work_name">{{ work.name }}</a></li>
+ {% empty %}
+ <li>No new work</li>
+ {% endfor %}
+</ul>
+
+<p>Created tags:</p>
+<ul>
+ {% for tag in created_tags %}
+ <li><a href="{{ tag.get_absolute_url }}"
+ class="tag_link">{{ tag.tag }}</a></li>
+ {% empty %}
+ <li>No new tag</li>
+ {% endfor %}
+</ul>
+
+<p>Created quotes:</p>
+{% for quote in created_quotes %}
+ {% include "quotes/display.html" with quote=quote %}
+{% endfor %}
+
+{% endblock %}
--- /dev/null
+{% extends 'quotes/base.html' %}
+
+{% block title %}Importing several quotes at once{% endblock %}
+
+{% block body %}
+
+<p>Expected format in the text area:</p>
+
+<pre>
+ Text of the quote
+ ---
+ Author of the quote
+ ---
+ Title of the work (book or play title, name of the speech, historical event, etc.)
+ ---
+ tag 1, tag 2, separated by commas
+ ===
+ A second quote
+ ---
+ Author of the second quote
+ ---
+ Title of the second work
+ ---
+ some, tags
+ ===
+ A third quote
+ ---
+ Author of the quote
+ ---
+ Title of the work
+ ---
+ etc, etc
+</pre>
+
+<form action="{% url 'quotes:massimport' %}" method="post">
+ {% csrf_token %}
+ <p>Quotes to import: </p>
+ <textarea name="quotes" cols="100" rows="20"></textarea>
+ <p><input type="submit" /></p>
+</form>
+
+{% endblock %}
--- /dev/null
+import pytest
+
+from .models import Author, Work, Quote, QuoteTag
+
+class Test_MassImport():
+ @pytest.fixture(scope='function')
+ def q1(self, db):
+ a1 = Author.objects.create(name="JFK")
+ w1 = Work.objects.create(name="Berlin speech", author=a1)
+ q1 = Quote.objects.create(text="<p>Ich bin...</p>", work=w1)
+ return q1
+
+ @pytest.mark.django_db
+ def test_massimport_1(self, q1, c):
+ allquotes = """\
+Ich bin ein Berliner
+---
+JFK
+---
+Berlin speech
+===
+To be or not to be, that is the question
+---
+William Shakespeare
+---
+Hamlet
+===
+To thine own self, be true
+---
+William Shakespeare
+---
+Hamlet
+---
+tag1, tag2, tag3
+===
+A rose by any other name...
+---
+William Shakespeare
+---
+Romeo and Juliet
+---
+tag1, tag555
+==="""
+
+ results = c.postPage('massimport/', {'quotes': allquotes})
+
+ assert "rejected" not in results
+
+ assert Quote.objects.get(text="<p>To thine own self, be true</p>")
+ assert Quote.objects.get(text__contains="To be or not to be")
+
+ assert Author.objects.get(name="JFK")
+ assert Author.objects.get(name="William Shakespeare")
+ assert Author.objects.all().count() == 2
+
+ hamlet = Work.objects.get(name="Hamlet")
+ assert hamlet
+ assert hamlet.author == Author.objects.get(name="William Shakespeare")
+
+ for quote in Quote.objects.all():
+ assert c.getPage(quote.get_absolute_url())
+
+ @pytest.mark.django_db
+ def test_massimport_2(self, q1, c):
+ allquotes = """\
+<script>somethingevil()</script>
+A rose by any other name...
+---
+William Shakespeare
+---
+Romeo and Juliet
+---
+tag1, tag555
+==="""
+
+ results = c.postPage('massimport/', {'quotes': allquotes})
+ assert "<script>" not in results
+ with pytest.raises(Quote.DoesNotExist):
+ Quote.objects.get(text__contains="<script>")
+
+ @pytest.mark.django_db
+ def test_massimport_3(self, c):
+ """Whitespace and stuff"""
+ allquotes = """\
+A rose by any other name...
+---
+William Shakespeare
+---
+Romeo and Juliet
+---
+tag1, tag555
+===
+
+To be, or not to be, that is the question:
+Whether 'tis Nobler in the mind to suffer
+The Slings and Arrows of outrageous Fortune,
+Or to take Arms against a Sea of troubles,
+And by opposing end them: to die, to sleep
+No more;
+
+---
+ William Shakespeare
+
+---
+Hamlet
+
+"""
+
+ results = c.postPage('massimport/', {'quotes': allquotes})
+ assert "rejected" not in results
+ assert Quote.objects.get(text="<p>A rose by any other name...</p>")
+ assert Author.objects.get(name="William Shakespeare")
+ assert Work.objects.get(name="Romeo and Juliet")
+ assert QuoteTag.objects.get(tag="tag1")
+ assert QuoteTag.objects.get(tag="tag555")
+
+ tirade = Quote.objects.get(text__contains="To be, or not to be")
+ assert tirade.work.name == "Hamlet"
+ assert tirade.work.author.name == "William Shakespeare"
+ assert Author.objects.filter(name__contains="Shakespeare").count() == 1
+
url(r'^cloud/$', views.cloud, name="cloud"),
url(r'^search/$', views.searchpage, name="search"),
+ url(r'^massimport/$', views.massimport, name="massimport"),
url(r'^all/$', views.all, name="all"),
]
from quotes.models import Author, Work, Quote, QuoteTag
import quotes.search as search
import quotes.tagcloud as tagcloud
+from quotes.massimport import domassimport
# create your views here.
def index(request):
def cloud(request):
clouddata = tagcloud.build_cloud()
return render(request, 'quotes/cloud.html', { 'cloud': clouddata })
+
+def massimport(request):
+ if 'quotes' in request.POST:
+ resultcontext = domassimport(request.POST['quotes'])
+ return render(request, 'quotes/domassimport.html', resultcontext)
+ else:
+ return render(request, 'quotes/massimport.html', {})