diff --git a/.gitignore b/.gitignore
index 4ff5976..2fe98c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,9 @@ build/
dist/
docs/_build
MANIFEST
+*.egg-info
+.tox/
+.cache/
+.coverage
+junit-*.xml
+coverage.xml
\ No newline at end of file
diff --git a/README.rst b/README.rst
index fa8391a..c6a94b1 100644
--- a/README.rst
+++ b/README.rst
@@ -1,6 +1,6 @@
-============================================================
- Frequently Asked Question (FAQ) management for Django apps
-============================================================
+==========================================================
+Frequently Asked Question (FAQ) management for Django apps
+==========================================================
This Django_ application provides the ability to create and manage lists of
Frequently Asked Questions (FAQ), organized by topic.
@@ -12,7 +12,7 @@ months.
.. _Django: http://www.djangoproject.com/
-TODO’s
+TODO's
------
Below are tasks that need done, features under consideration, and some
diff --git a/docs/conf.py b/docs/conf.py
index d78d4b7..f701f1f 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -46,8 +46,8 @@
master_doc = 'index'
# General information about the project.
-project = u'django-faq'
-copyright = u'2012, Ben Spaulding'
+project = 'django-faq'
+copyright = '2012, Ben Spaulding'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -184,8 +184,8 @@
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
- ('index', 'django-faq.tex', u'django-faq Documentation',
- u'Ben Spaulding', 'manual'),
+ ('index', 'django-faq.tex', 'django-faq Documentation',
+ 'Ben Spaulding', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -217,8 +217,8 @@
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
- ('index', 'django-faq', u'django-faq Documentation',
- [u'Ben Spaulding'], 1)
+ ('index', 'django-faq', 'django-faq Documentation',
+ ['Ben Spaulding'], 1)
]
diff --git a/faq/__init__.py b/faq/__init__.py
index 9749719..60f1312 100644
--- a/faq/__init__.py
+++ b/faq/__init__.py
@@ -15,10 +15,23 @@
"""
-from django.utils.translation import ugettext_lazy as _
-
-
-__version__ = '0.8.3'
-
-# Mark the app_label for translation.
-_(u'faq')
+__version_info__ = {
+ 'major': 1,
+ 'minor': 0,
+ 'micro': 0,
+ 'releaselevel': 'final',
+ 'serial': 1
+}
+
+
+def get_version(short=False):
+ assert __version_info__['releaselevel'] in ('alpha', 'beta', 'final')
+ vers = ["%(major)i.%(minor)i" % __version_info__, ]
+ if __version_info__['micro']:
+ vers.append(".%(micro)i" % __version_info__)
+ if __version_info__['releaselevel'] != 'final' and not short:
+ vers.append('%s%i' % (
+ __version_info__['releaselevel'][0], __version_info__['serial']))
+ return ''.join(vers)
+
+__version__ = get_version()
diff --git a/faq/admin.py b/faq/admin.py
index 9171e04..e6a7d98 100644
--- a/faq/admin.py
+++ b/faq/admin.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from django.contrib import admin
-from django.contrib.sites.models import Site
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_noop, ungettext
@@ -26,8 +25,8 @@ def update_status(modeladmin, request, queryset, status):
obj.save()
# Now log what happened.
# Use ugettext_noop() 'cause this is going straight into the db.
- log_message = ugettext_noop(u'Changed status to \'%s\'.' %
- obj.get_status_display())
+ log_message = ugettext_noop('Changed status to \'%s\'.' %
+ obj.get_status_display())
modeladmin.log_change(request, obj, log_message)
# Send a message to the user telling them what has happened.
@@ -39,8 +38,8 @@ def update_status(modeladmin, request, queryset, status):
if not message_dict['count'] == 1:
message_dict['object'] = modeladmin.model._meta.verbose_name_plural
user_message = ungettext(
- u'%(count)s %(object)s was successfully %(verb)s.',
- u'%(count)s %(object)s were successfully %(verb)s.',
+ '%(count)s %(object)s was successfully %(verb)s.',
+ '%(count)s %(object)s were successfully %(verb)s.',
message_dict['count']) % message_dict
modeladmin.message_user(request, user_message)
@@ -53,19 +52,19 @@ def update_status(modeladmin, request, queryset, status):
def draft(modeladmin, request, queryset):
"""Admin action for setting status of selected items to 'drafted'."""
return update_status(modeladmin, request, queryset, DRAFTED)
-draft.short_description = _(u'Draft selected %(verbose_name_plural)s')
+draft.short_description = _('Draft selected %(verbose_name_plural)s')
def publish(modeladmin, request, queryset):
"""Admin action for setting status of selected items to 'published'."""
return update_status(modeladmin, request, queryset, PUBLISHED)
-publish.short_description = _(u'Publish selected %(verbose_name_plural)s')
+publish.short_description = _('Publish selected %(verbose_name_plural)s')
def remove(modeladmin, request, queryset):
"""Admin action for setting status of selected items to 'removed'."""
return update_status(modeladmin, request, queryset, REMOVED)
-remove.short_description = _(u'Remove selected %(verbose_name_plural)s')
+remove.short_description = _('Remove selected %(verbose_name_plural)s')
# Inlines.
@@ -99,14 +98,13 @@ class TopicAdmin(FAQAdminBase):
def question_count(self, obj):
"""Returns the total number of Questions for this topic."""
return obj.questions.count()
- question_count.short_description = _(u'No. of Questions')
+ question_count.short_description = _('No. of Questions')
class QuestionAdmin(FAQAdminBase):
fieldsets = (
(None, {
- 'fields': ('topic', 'question', 'slug', 'answer', 'status',
- 'ordering')}),
+ 'fields': ('topic', 'question', 'slug', 'answer', 'status', 'ordering')}),
)
list_display = ('question', 'topic', 'status', 'ordering')
list_filter = ('status', 'topic', 'modified', 'created')
diff --git a/faq/fixtures/test_data.json b/faq/fixtures/test_data.json
index 7981377..9f9033e 100644
--- a/faq/fixtures/test_data.json
+++ b/faq/fixtures/test_data.json
@@ -1,206 +1,206 @@
[
{
- "pk": 3,
- "model": "faq.topic",
+ "pk": 3,
+ "model": "faq.topic",
"fields": {
- "status": 3,
- "description": "All about \u2026 US!",
- "title": "About us",
- "created": "2011-06-07 15:21:44",
- "modified": "2011-06-07 15:30:31",
+ "status": 3,
+ "description": "All about \u2026 US!",
+ "title": "About us",
+ "created": "2011-06-07 15:21:44Z",
+ "modified": "2011-06-07 15:30:31Z",
"sites": [
2
- ],
+ ],
"slug": "about-us"
}
- },
+ },
{
- "pk": 5,
- "model": "faq.topic",
+ "pk": 5,
+ "model": "faq.topic",
"fields": {
- "status": 3,
- "description": "Sshhh!",
- "title": "Black market items",
- "created": "2011-06-07 15:26:07",
- "modified": "2011-06-07 15:26:07",
+ "status": 3,
+ "description": "Sshhh!",
+ "title": "Black market items",
+ "created": "2011-06-07 15:26:07Z",
+ "modified": "2011-06-07 15:26:07Z",
"sites": [
1
- ],
+ ],
"slug": "black-market-items"
}
- },
+ },
{
- "pk": 2,
- "model": "faq.topic",
+ "pk": 2,
+ "model": "faq.topic",
"fields": {
- "status": 2,
- "description": "All about returning an unwanted item.",
- "title": "Returns",
- "created": "2011-06-07 11:33:25",
- "modified": "2011-06-07 15:29:41",
+ "status": 2,
+ "description": "All about returning an unwanted item.",
+ "title": "Returns",
+ "created": "2011-06-07 11:33:25Z",
+ "modified": "2011-06-07 15:29:41Z",
"sites": [
- 1,
+ 1,
2
- ],
+ ],
"slug": "returns"
}
- },
+ },
{
- "pk": 1,
- "model": "faq.topic",
+ "pk": 1,
+ "model": "faq.topic",
"fields": {
- "status": 2,
- "description": "All about how we ship your purchase to you.",
- "title": "Shipping",
- "created": "2011-06-07 11:31:15",
- "modified": "2011-06-07 15:28:46",
+ "status": 2,
+ "description": "All about how we ship your purchase to you.",
+ "title": "Shipping",
+ "created": "2011-06-07 11:31:15Z",
+ "modified": "2011-06-07 15:28:46Z",
"sites": [
1
- ],
+ ],
"slug": "shipping"
}
- },
+ },
{
- "pk": 4,
- "model": "faq.topic",
+ "pk": 4,
+ "model": "faq.topic",
"fields": {
- "status": 1,
- "description": "About our website.",
- "title": "Website",
- "created": "2011-06-07 15:23:59",
- "modified": "2011-06-07 15:23:59",
+ "status": 1,
+ "description": "About our website.",
+ "title": "Website",
+ "created": "2011-06-07 15:23:59Z",
+ "modified": "2011-06-07 15:23:59Z",
"sites": [
- 1,
+ 1,
2
- ],
+ ],
"slug": "website"
}
- },
+ },
{
- "pk": 4,
- "model": "faq.question",
+ "pk": 4,
+ "model": "faq.question",
"fields": {
- "status": 2,
- "created": "2011-06-07 15:21:44",
- "ordering": 1,
- "question": "Are you hiring?",
- "modified": "2011-06-07 15:21:44",
- "topic": 3,
- "answer": "Yes and no. If you are awesome, yes. If you are less than awesome, then not so much.",
+ "status": 2,
+ "created": "2011-06-07 15:21:44Z",
+ "ordering": 1,
+ "question": "Are you hiring?",
+ "modified": "2011-06-07 15:21:44Z",
+ "topic": 3,
+ "answer": "Yes and no. If you are awesome, yes. If you are less than awesome, then not so much.",
"slug": "are-you-hiring"
}
- },
+ },
{
- "pk": 5,
- "model": "faq.question",
+ "pk": 5,
+ "model": "faq.question",
"fields": {
- "status": 2,
- "created": "2011-06-07 15:23:59",
- "ordering": 1,
- "question": "Do you have an SLA?",
- "modified": "2011-06-07 15:23:59",
- "topic": 4,
- "answer": "No, because people with those get hosed.",
+ "status": 2,
+ "created": "2011-06-07 15:23:59Z",
+ "ordering": 1,
+ "question": "Do you have an SLA?",
+ "modified": "2011-06-07 15:23:59Z",
+ "topic": 4,
+ "answer": "No, because people with those get hosed.",
"slug": "do-you-have-an-sla"
}
- },
+ },
{
- "pk": 6,
- "model": "faq.question",
+ "pk": 6,
+ "model": "faq.question",
"fields": {
- "status": 2,
- "created": "2011-06-07 15:26:07",
- "ordering": 1,
- "question": "How do you acquire black market items?",
- "modified": "2011-06-07 15:26:07",
- "topic": 5,
- "answer": "Very, very carefully",
+ "status": 2,
+ "created": "2011-06-07 15:26:07Z",
+ "ordering": 1,
+ "question": "How do you acquire black market items?",
+ "modified": "2011-06-07 15:26:07Z",
+ "topic": 5,
+ "answer": "Very, very carefully",
"slug": "how-do-you-acquire-black-market-items"
}
- },
+ },
{
- "pk": 3,
- "model": "faq.question",
+ "pk": 3,
+ "model": "faq.question",
"fields": {
- "status": 2,
- "created": "2011-06-07 11:33:25",
- "ordering": 1,
- "question": "How long do I have to return my item?",
- "modified": "2011-06-07 11:33:25",
- "topic": 2,
- "answer": "We are very liberal with this, but it is generally 20-30 minutes.",
+ "status": 2,
+ "created": "2011-06-07 11:33:25Z",
+ "ordering": 1,
+ "question": "How long do I have to return my item?",
+ "modified": "2011-06-07 11:33:25Z",
+ "topic": 2,
+ "answer": "We are very liberal with this, but it is generally 20-30 minutes.",
"slug": "how-long-do-i-have-to-return-my-item"
}
- },
+ },
{
- "pk": 1,
- "model": "faq.question",
+ "pk": 1,
+ "model": "faq.question",
"fields": {
- "status": 2,
- "created": "2011-06-07 11:31:15",
- "ordering": 1,
- "question": "How much does shipping cost?",
- "modified": "2011-06-07 11:31:15",
- "topic": 1,
- "answer": "Not much. At last check it was just shy of one frillion dollars.",
+ "status": 2,
+ "created": "2011-06-07 11:31:15Z",
+ "ordering": 1,
+ "question": "How much does shipping cost?",
+ "modified": "2011-06-07 11:31:15Z",
+ "topic": 1,
+ "answer": "Not much. At last check it was just shy of one frillion dollars.",
"slug": "how-much-does-shipping-cost"
}
- },
+ },
{
- "pk": 2,
- "model": "faq.question",
+ "pk": 2,
+ "model": "faq.question",
"fields": {
- "status": 2,
- "created": "2011-06-07 11:32:14",
- "ordering": 2,
- "question": "How fast will my item arrive?",
- "modified": "2011-06-07 11:32:14",
- "topic": 1,
- "answer": "Sometime next week. We think.",
+ "status": 2,
+ "created": "2011-06-07 11:32:14Z",
+ "ordering": 2,
+ "question": "How fast will my item arrive?",
+ "modified": "2011-06-07 11:32:14Z",
+ "topic": 1,
+ "answer": "Sometime next week. We think.",
"slug": "how-fast-will-my-item-arrive"
}
- },
+ },
{
- "pk": 7,
- "model": "faq.question",
+ "pk": 7,
+ "model": "faq.question",
"fields": {
- "status": 1,
- "created": "2011-06-07 15:28:46",
- "ordering": 3,
- "question": "In what color box do you ship?",
- "modified": "2011-06-07 15:28:46",
- "topic": 1,
- "answer": "Green, because it is good for the environment. Go us!",
+ "status": 1,
+ "created": "2011-06-07 15:28:46Z",
+ "ordering": 3,
+ "question": "In what color box do you ship?",
+ "modified": "2011-06-07 15:28:46Z",
+ "topic": 1,
+ "answer": "Green, because it is good for the environment. Go us!",
"slug": "in-what-color-box-do-you-ship"
}
- },
+ },
{
- "pk": 8,
- "model": "faq.question",
+ "pk": 8,
+ "model": "faq.question",
"fields": {
- "status": 3,
- "created": "2011-06-07 15:28:46",
- "ordering": 4,
- "question": "What carrier do you use?",
- "modified": "2011-06-07 15:28:46",
- "topic": 1,
- "answer": "Whomever saves us the most money.",
+ "status": 3,
+ "created": "2011-06-07 15:28:46Z",
+ "ordering": 4,
+ "question": "What carrier do you use?",
+ "modified": "2011-06-07 15:28:46Z",
+ "topic": 1,
+ "answer": "Whomever saves us the most money.",
"slug": "what-carrier-do-you-use"
}
- },
+ },
{
- "pk": 1,
- "model": "sites.site",
+ "pk": 1,
+ "model": "sites.site",
"fields": {
- "domain": "example.com",
+ "domain": "example.com",
"name": "example.com"
}
- },
+ },
{
- "pk": 2,
- "model": "sites.site",
+ "pk": 2,
+ "model": "sites.site",
"fields": {
- "domain": "example.us",
+ "domain": "example.us",
"name": "example.us"
}
}
diff --git a/faq/forms.py b/faq/forms.py
index afcab17..20d44eb 100644
--- a/faq/forms.py
+++ b/faq/forms.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
+from builtins import object
from django import forms
from faq.models import Question
@@ -8,7 +9,7 @@
class QuestionForm(forms.ModelForm):
"""A form whose only purpose is to manage fields for the QuestionInline."""
- class Meta:
+ class Meta(object):
# InlineModelAdmin does not support ``fields``, so if we want to order
# the fields in an InlineModelAdmin, we must do so with a custom
# ModelForm. This is not ideal, but at least it gets the job done.
diff --git a/faq/migrations/0001_initial.py b/faq/migrations/0001_initial.py
new file mode 100644
index 0000000..6d5b18a
--- /dev/null
+++ b/faq/migrations/0001_initial.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('sites', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Question',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('created', models.DateTimeField(auto_now_add=True, verbose_name='date created')),
+ ('modified', models.DateTimeField(auto_now=True, verbose_name='date modified')),
+ ('status', models.IntegerField(default=1, help_text='Only objects with "published" status will be displayed publicly.', db_index=True, verbose_name='status', choices=[(1, 'drafted'), (2, 'published'), (3, 'removed')])),
+ ('question', models.CharField(max_length=255, verbose_name='question')),
+ ('slug', models.SlugField(help_text='Used in the URL for the Question. Must be unique.', unique=True, verbose_name='slug')),
+ ('answer', models.TextField(verbose_name='answer')),
+ ('ordering', models.PositiveSmallIntegerField(help_text='An integer used to order the question amongst others related to the same topic. If not given this question will be last in the list.', db_index=True, verbose_name='ordering', blank=True)),
+ ],
+ options={
+ 'ordering': ('ordering', 'question', 'slug'),
+ 'abstract': False,
+ 'get_latest_by': 'modified',
+ 'verbose_name': 'question',
+ 'verbose_name_plural': 'questions',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Topic',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('created', models.DateTimeField(auto_now_add=True, verbose_name='date created')),
+ ('modified', models.DateTimeField(auto_now=True, verbose_name='date modified')),
+ ('status', models.IntegerField(default=1, help_text='Only objects with "published" status will be displayed publicly.', db_index=True, verbose_name='status', choices=[(1, 'drafted'), (2, 'published'), (3, 'removed')])),
+ ('title', models.CharField(max_length=255, verbose_name='title')),
+ ('slug', models.SlugField(help_text='Used in the URL for the topic. Must be unique.', unique=True, verbose_name='slug')),
+ ('description', models.TextField(help_text='A short description of this topic.', verbose_name='description', blank=True)),
+ ('sites', models.ManyToManyField(related_name='faq_topics', verbose_name='sites', to='sites.Site')),
+ ],
+ options={
+ 'ordering': ('title', 'slug'),
+ 'abstract': False,
+ 'get_latest_by': 'modified',
+ 'verbose_name': 'topic',
+ 'verbose_name_plural': 'topics',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.AddField(
+ model_name='question',
+ name='topic',
+ field=models.ForeignKey(related_name='questions', verbose_name='topic', to='faq.Topic'),
+ preserve_default=True,
+ ),
+ ]
diff --git a/faq/migrations/__init__.py b/faq/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/faq/models.py b/faq/models.py
index 589573d..55611b2 100644
--- a/faq/models.py
+++ b/faq/models.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
+from builtins import object
from django.db import models
from django.conf import settings
from django.contrib.sites.models import Site
@@ -50,21 +51,21 @@ class OnSiteManager(models.Manager):
def on_site(self):
"""Returns only items related to the current site."""
- return self.get_query_set().filter(**_field_lookups(self.model))
+ return self.get_queryset().filter(**_field_lookups(self.model))
def drafted(self):
"""Returns only on-site items with a status of 'drafted'."""
- return self.get_query_set().filter(
+ return self.get_queryset().filter(
**_field_lookups(self.model, DRAFTED))
def published(self):
"""Returns only on-site items with a status of 'published'."""
- return self.get_query_set().filter(
+ return self.get_queryset().filter(
**_field_lookups(self.model, PUBLISHED))
def removed(self):
"""Returns only on-site items with a status of 'removed'."""
- return self.get_query_set().filter(
+ return self.get_queryset().filter(
**_field_lookups(self.model, REMOVED))
@@ -73,16 +74,20 @@ def removed(self):
class FAQBase(models.Model):
"""A model holding information common to Topics and Questions."""
- created = models.DateTimeField(_(u'date created'), auto_now_add=True)
- modified = models.DateTimeField(_(u'date modified'), auto_now=True)
- status = models.IntegerField(_(u'status'), choices=STATUS_CHOICES,
+ created = models.DateTimeField(_('date created'), auto_now_add=True)
+ modified = models.DateTimeField(_('date modified'), auto_now=True)
+ status = models.IntegerField(
+ _('status'),
+ choices=STATUS_CHOICES,
# TODO: Genericize/fix the help_text.
- db_index=True, default=DRAFTED, help_text=_(u'Only objects with \
- "published" status will be displayed publicly.'))
+ db_index=True,
+ default=DRAFTED,
+ help_text=_('Only objects with "published" '
+ 'status will be displayed publicly.'))
objects = OnSiteManager()
- class Meta:
+ class Meta(object):
abstract = True
get_latest_by = 'modified'
@@ -90,20 +95,24 @@ class Meta:
class Topic(FAQBase):
"""A topic that a Question can belong to."""
- title = models.CharField(_(u'title'), max_length=255)
- slug = models.SlugField(_(u'slug'), unique=True, help_text=_(u'Used in \
+ title = models.CharField(_('title'), max_length=255)
+ slug = models.SlugField(_('slug'), unique=True, help_text=_('Used in \
the URL for the topic. Must be unique.'))
- description = models.TextField(_(u'description'), blank=True,
- help_text=_(u'A short description of this topic.'))
- sites = models.ManyToManyField(Site, verbose_name=_(u'sites'),
+ description = models.TextField(
+ _('description'),
+ blank=True,
+ help_text=_('A short description of this topic.'))
+ sites = models.ManyToManyField(
+ Site,
+ verbose_name=_('sites'),
related_name='faq_topics')
class Meta(FAQBase.Meta):
ordering = ('title', 'slug')
- verbose_name = _(u'topic')
- verbose_name_plural = _(u'topics')
+ verbose_name = _('topic')
+ verbose_name_plural = _('topics')
- def __unicode__(self):
+ def __str__(self):
return self.title
@models.permalink
@@ -114,23 +123,28 @@ def get_absolute_url(self):
class Question(FAQBase):
"""A frequently asked question."""
- question = models.CharField(_(u'question'), max_length=255)
- slug = models.SlugField(_(u'slug'), unique=True, help_text=_(u'Used in \
+ question = models.CharField(_('question'), max_length=255)
+ slug = models.SlugField(_('slug'), unique=True, help_text=_('Used in \
the URL for the Question. Must be unique.'))
- answer = models.TextField(_(u'answer'))
- topic = models.ForeignKey(Topic, verbose_name=_(u'topic'),
+ answer = models.TextField(_('answer'))
+ topic = models.ForeignKey(
+ Topic,
+ verbose_name=_('topic'),
related_name='questions')
- ordering = models.PositiveSmallIntegerField(_(u'ordering'), blank=True,
- db_index=True, help_text=_(u'An integer used to order the question \
+ ordering = models.PositiveSmallIntegerField(
+ _('ordering'),
+ blank=True,
+ db_index=True,
+ help_text=_('An integer used to order the question \
amongst others related to the same topic. If not given this \
question will be last in the list.'))
class Meta(FAQBase.Meta):
ordering = ('ordering', 'question', 'slug')
- verbose_name = _(u'question')
- verbose_name_plural = _(u'questions')
+ verbose_name = _('question')
+ verbose_name_plural = _('questions')
- def __unicode__(self):
+ def __str__(self):
return self.question
def save(self, *args, **kwargs):
@@ -158,5 +172,6 @@ def save(self, *args, **kwargs):
@models.permalink
def get_absolute_url(self):
- return ('faq-question-detail', (), {'topic_slug': self.topic.slug,
+ return ('faq-question-detail', (), {
+ 'topic_slug': self.topic.slug,
'slug': self.slug})
diff --git a/faq/search_indexes.py b/faq/search_indexes.py
index ebc93c6..4f81fd4 100644
--- a/faq/search_indexes.py
+++ b/faq/search_indexes.py
@@ -12,7 +12,7 @@
from haystack import indexes
-from faq.settings import SEARCH_INDEX
+from faq.settings import SEARCH_INDEX, REGISTER_SEARCH
from faq.models import Topic, Question
@@ -61,9 +61,10 @@ def get_queryset(self):
# try/except in order to register search indexes with site for Haystack 1.X
# without throwing exceptions with Haystack 2.0.
-try:
- from haystack.sites import site
- site.register(Topic, TopicIndex)
- site.register(Question, QuestionIndex)
-except ImportError:
- pass
+if REGISTER_SEARCH:
+ try:
+ from haystack.sites import site
+ site.register(Topic, TopicIndex)
+ site.register(Question, QuestionIndex)
+ except ImportError:
+ pass
diff --git a/faq/settings.py b/faq/settings.py
index f37fc0d..6f2bb9b 100644
--- a/faq/settings.py
+++ b/faq/settings.py
@@ -13,12 +13,13 @@
REMOVED = getattr(settings, 'FAQ_REMOVED', 3)
STATUS_CHOICES = (
- (DRAFTED, _(u'drafted')),
- (PUBLISHED, _(u'published')),
- (REMOVED, _(u'removed')),
+ (DRAFTED, _('drafted')),
+ (PUBLISHED, _('published')),
+ (REMOVED, _('removed')),
)
STATUS_CHOICES = getattr(settings, 'FAQ_STATUS_CHOICES', STATUS_CHOICES)
+REGISTER_SEARCH = getattr(settings, 'FAQ_REGISTER_SEARCH', True)
# Haystack settings.
# The default search index used for the app is the default haystack index.
diff --git a/faq/templates/faq/question_detail.html b/faq/templates/faq/question_detail.html
index 8f653cf..a0f312b 100644
--- a/faq/templates/faq/question_detail.html
+++ b/faq/templates/faq/question_detail.html
@@ -9,7 +9,7 @@
{% block header %}
- {% trans "FAQ" %} ›
+ {% trans "FAQ" %} ›
{{ topic }} ›
{{ question }}
{% endblock %}
diff --git a/faq/templates/faq/topic_detail.html b/faq/templates/faq/topic_detail.html
index 53dca0b..4cc564e 100644
--- a/faq/templates/faq/topic_detail.html
+++ b/faq/templates/faq/topic_detail.html
@@ -13,7 +13,7 @@
{% block header %}
- {% trans "FAQ" %} › {{ topic }}
+ {% trans "FAQ" %} › {{ topic }}
{% endblock %}
diff --git a/faq/tests/__init__.py b/faq/tests/__init__.py
index 6cad6ac..4ce8083 100644
--- a/faq/tests/__init__.py
+++ b/faq/tests/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-from django.test import TestCase
+from django.test import TestCase, override_settings
from django.contrib.sites.models import Site
from faq.settings import DRAFTED
@@ -12,6 +12,7 @@
# Test admin actions?
##
+
class BaseTestCase(TestCase):
""""""
@@ -22,8 +23,8 @@ def setUp(self):
# data because we will be testing for the state of something newly
# created, which the test data does not contain, obviously.
self.topics = {
- 'new': Site.objects.get_current().faq_topics.create(title=u'Test Topic',
- slug=u'test-topic'),
+ 'new': Site.objects.get_current().faq_topics.create(title='Test Topic',
+ slug='test-topic'),
'drafted': Topic.objects.get(slug='website'),
'published': Topic.objects.get(slug='shipping'),
'removed': Topic.objects.get(slug='black-market-items'),
@@ -31,10 +32,10 @@ def setUp(self):
}
self. questions = {
- 'new1': self.topics['new'].questions.create(question=u'Where am I?',
- answer=u'That is classified.'),
- 'new2': self.topics['new'].questions.create(question=u'Who are you?',
- answer=u'I cannot say.'),
+ 'new1': self.topics['new'].questions.create(question='Where am I?',
+ answer='That is classified.'),
+ 'new2': self.topics['new'].questions.create(question='Who are you?',
+ answer='I cannot say.'),
'drafted': Question.objects.get(slug='in-what-color-box-do-you-ship'),
'published': Question.objects.get(slug='how-much-does-shipping-cost'),
'removed': Question.objects.get(slug='what-carrier-do-you-use'),
@@ -55,32 +56,24 @@ class ManagerTestCase(BaseTestCase):
class ModelsTestCase(BaseTestCase):
""""""
- def testManager(self):
+ def test_manager(self):
# Because of our sublcassing with the models, be certain that the
# manager is wired up correctly.
self.assertTrue(isinstance(Topic.objects, OnSiteManager))
self.assertTrue(isinstance(Question.objects, OnSiteManager))
- def testUnicode(self):
- # Ensure that we don't absent-mindedly change what the `__unicode__()`
- # method returns.
- self.assertEqual(self.topics['new'].__unicode__(),
- self.topics['new'].title)
- self.assertEqual(self.questions['new1'].__unicode__(),
- self.questions['new1'].question)
-
- def testDefaultStatus(self):
+ def test_default_status(self):
# Items created without choosing a status should be drafted by default.
self.assertEqual(self.topics['new'].status, DRAFTED)
self.assertEqual(self.questions['new1'].status, DRAFTED)
- def testSlugOnSave(self):
+ def test_slug_on_save(self):
# Be sure we are properly creating slugs for questions that are created
# without them (those created as an inline to a topic).
- self.assertEqual(self.questions['new1'].slug, u'where-am-i')
- self.assertEqual(self.questions['new2'].slug, u'who-are-you')
+ self.assertEqual(self.questions['new1'].slug, 'where-am-i')
+ self.assertEqual(self.questions['new2'].slug, 'who-are-you')
- def testOrderingOnSave(self):
+ def test_ordering_on_save(self):
# Be sure we are properly calculating and filling the ordering field
# when a user leaves it blank.
self.assertEqual(self.questions['new1'].ordering, 1)
@@ -107,27 +100,13 @@ def setUp(self):
}
-class ViewsShallowTestCase(ViewsBaseTestCase):
-
- urls = 'faq.urls.shallow'
-
- def testTopicDetail(self):
- # Redirects to a fragment identifier on the topic list.
- self.assertRedirects(self.responses['topic_detail'],
- '/#shipping', status_code=301)
-
- def testQuestionDetail(self):
- # Redirects to a fragment identifier on the topic list.
- self.assertRedirects(self.responses['question_detail'],
- '/#how-much-does-shipping-cost', status_code=301)
-
+@override_settings(ROOT_URLCONF='faq.urls.normal')
+class ViewsNormalTestCase(ViewsBaseTestCase):
+ def test_import_view(self):
+ # This was causing an issue for some reason
+ from faq.views import TopicListView, TopicDetailView, question_detail # NOQA
-class ViewsNormalTestCase(ViewsShallowTestCase):
- """"""
-
- urls = 'faq.urls.normal'
-
- def testTopicDetail(self):
+ def test_topic_detail(self):
# Does not redirect.
self.assertEqual(self.responses['topic_detail'].status_code, 200)
# Check for our extra_context.
@@ -135,18 +114,15 @@ def testTopicDetail(self):
self.assertEqual(list(self.responses['topic_detail'].context['question_list']),
list(self.topics['published'].questions.published()))
- def testQuestionDetail(self):
+ def test_question_detail(self):
# Redirects to a fragment identifier on the topic detail.
self.assertRedirects(self.responses['question_detail'],
'/shipping/#how-much-does-shipping-cost', status_code=301)
+@override_settings(ROOT_URLCONF='faq.urls.deep')
class ViewsDeepTestCase(ViewsNormalTestCase):
- """"""
-
- urls = 'faq.urls.deep'
-
- def testQuestionDetail(self):
+ def test_question_detail(self):
# Does not redirect.
self.assertEqual(self.responses['question_detail'].status_code, 200)
# Check for our extra_context.
diff --git a/faq/urls/deep.py b/faq/urls/deep.py
index e5b111a..75e0c7c 100644
--- a/faq/urls/deep.py
+++ b/faq/urls/deep.py
@@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
-from django.conf.urls.defaults import *
+from django.conf.urls import url
-from faq.views.shallow import topic_list
-from faq.views.normal import topic_detail
-from faq.views.deep import question_detail
+from faq.views import TopicListView, TopicDetailView, QuestionDetailView
# Include these patterns if you want URLs like:
@@ -14,9 +12,14 @@
# /faq/topic/question/
#
-urlpatterns = patterns('',
- url(r'^$', topic_list, name='faq-topic-list'),
- url(r'^(?P[-\w]+)/$', topic_detail, name='faq-topic-detail'),
- url(r'^(?P[-\w]+)/(?P[-\w]+)/$', question_detail,
+urlpatterns = [
+ url(r'^$', TopicListView.as_view(), name='faq-topic-list'),
+ url(
+ r'^(?P[-\w]+)/$',
+ TopicDetailView.as_view(),
+ name='faq-topic-detail'),
+ url(
+ r'^(?P[-\w]+)/(?P[-\w]+)/$',
+ QuestionDetailView.as_view(),
name='faq-question-detail'),
-)
+]
diff --git a/faq/urls/normal.py b/faq/urls/normal.py
index 49e39cc..927c35a 100644
--- a/faq/urls/normal.py
+++ b/faq/urls/normal.py
@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
-from django.conf.urls.defaults import *
+from django.conf.urls import url
-from faq.views.shallow import topic_list
-from faq.views.normal import topic_detail, question_detail
+from faq.views import TopicListView, TopicDetailView, question_detail
# Include these patterns if you want URLs like:
@@ -13,9 +12,14 @@
# /faq/topic/#question
#
-urlpatterns = patterns('',
- url(r'^$', topic_list, name='faq-topic-list'),
- url(r'^(?P[-\w]+)/$', topic_detail, name='faq-topic-detail'),
- url(r'^(?P[-\w]+)/(?P[-\w]+)/$', question_detail,
+urlpatterns = [
+ url(r'^$', TopicListView.as_view(), name='faq-topic-list'),
+ url(
+ r'^(?P[-\w]+)/$',
+ TopicDetailView.as_view(),
+ name='faq-topic-detail'),
+ url(
+ r'^(?P[-\w]+)/#(?P[-\w]+)/$',
+ question_detail,
name='faq-question-detail'),
-)
+]
diff --git a/faq/urls/shallow.py b/faq/urls/shallow.py
deleted file mode 100644
index a70f2a6..0000000
--- a/faq/urls/shallow.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from django.conf.urls.defaults import *
-
-from faq.views.shallow import topic_list, topic_detail, question_detail
-
-
-# Include these patterns if you want URLs like:
-#
-# /faq/
-# /faq/#topic
-# /faq/#question
-#
-
-urlpatterns = patterns('',
- url(r'^$', topic_list, name='faq-topic-list'),
- url(r'^(?P[-\w]+)/$', topic_detail, name='faq-topic-detail'),
- url(r'^(?P[-\w]+)/(?P[-\w]+)/$', question_detail,
- name='faq-question-detail'),
-)
diff --git a/faq/views.py b/faq/views.py
new file mode 100644
index 0000000..ceff64b
--- /dev/null
+++ b/faq/views.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+from django.core.urlresolvers import reverse
+from django.shortcuts import get_object_or_404, redirect
+from django.views.generic.detail import DetailView
+from django.views.generic.list import ListView
+
+from faq.models import Topic, Question
+
+
+def _fragmentify(model, slug, url=None):
+ get_object_or_404(model.objects.published().filter(slug=slug))
+ url = url or reverse('faq-topic-list')
+ fragment = '#%s' % slug
+
+ return redirect(url + fragment, permanent=True)
+
+
+class QuestionDetailView(DetailView):
+ model = Question
+
+ def get_context_data(self, **kwargs):
+ context = super(QuestionDetailView, self).get_context_data(**kwargs)
+ context['topic'] = Topic.objects.published().get(
+ slug=context['question'].topic.slug
+ )
+ return context
+
+ def get_template_names(self):
+ return ['faq/question_detail.html']
+
+
+class TopicDetailView(DetailView):
+ model = Topic
+
+ def get_context_data(self, **kwargs):
+ context = super(TopicDetailView, self).get_context_data(**kwargs)
+ context['question_list'] = Question.objects.published().filter(
+ topic__slug=context['topic'].slug
+ )
+ return context
+
+ def get_template_names(self):
+ return ['faq/topic_detail.html']
+
+
+class TopicListView(ListView):
+ model = Topic
+
+ def get_template_names(self):
+ return ['faq/topic_list.html']
+
+
+def question_detail(request, topic_slug, slug):
+ """
+ A detail view of a Question.
+
+ Simply redirects to a detail page for the related :model:`faq.Topic`
+ (:view:`faq.views.topic_detail`) with the addition of a fragment
+ identifier that links to the given :model:`faq.Question`.
+ E.g. ``/faq/topic-slug/#question-slug``.
+
+ """
+ url = reverse('faq-topic-detail', kwargs={'slug': topic_slug})
+ return _fragmentify(Question, slug, url)
+
+
+def topic_detail(request, slug):
+ """
+ A detail view of a Topic
+
+ Simply redirects to :view:`faq.views.topic_list` with the addition of
+ a fragment identifier that links to the given :model:`faq.Topic`.
+ E.g., ``/faq/#topic-slug``.
+
+ """
+ return _fragmentify(Topic, slug)
diff --git a/faq/views/__init__.py b/faq/views/__init__.py
deleted file mode 100644
index 40a96af..0000000
--- a/faq/views/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# -*- coding: utf-8 -*-
diff --git a/faq/views/deep.py b/faq/views/deep.py
deleted file mode 100644
index 55b273b..0000000
--- a/faq/views/deep.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from django.views.generic.list_detail import object_detail
-
-from faq.models import Topic, Question
-
-
-def question_detail(request, topic_slug, slug):
- """
- A detail view of a Question.
-
- Templates:
- :template:`faq/question_detail.html`
- Context:
- question
- A :model:`faq.Question`.
- topic
- The :model:`faq.Topic` object related to ``question``.
-
- """
- extra_context = {
- 'topic': Topic.objects.published().get(slug=topic_slug),
- }
-
- return object_detail(request, queryset=Question.objects.published(),
- extra_context=extra_context, template_object_name='question',
- slug=slug)
diff --git a/faq/views/normal.py b/faq/views/normal.py
deleted file mode 100644
index ad1004a..0000000
--- a/faq/views/normal.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from django.core.urlresolvers import reverse
-from django.shortcuts import get_object_or_404, redirect
-from django.views.generic.list_detail import object_detail
-
-from faq.models import Topic, Question
-from faq.views.shallow import _fragmentify
-
-
-def topic_detail(request, slug):
- """
- A detail view of a Topic
-
- Templates:
- :template:`faq/topic_detail.html`
- Context:
- topic
- An :model:`faq.Topic` object.
- question_list
- A list of all published :model:`faq.Question` objects that relate
- to the given :model:`faq.Topic`.
-
- """
- extra_context = {
- 'question_list': Question.objects.published().filter(topic__slug=slug),
- }
-
- return object_detail(request, queryset=Topic.objects.published(),
- extra_context=extra_context, template_object_name='topic', slug=slug)
-
-
-def question_detail(request, topic_slug, slug):
- """
- A detail view of a Question.
-
- Simply redirects to a detail page for the related :model:`faq.Topic`
- (:view:`faq.views.topic_detail`) with the addition of a fragment
- identifier that links to the given :model:`faq.Question`.
- E.g. ``/faq/topic-slug/#question-slug``.
-
- """
- url = reverse('faq-topic-detail', kwargs={'slug': topic_slug})
- return _fragmentify(Question, slug, url)
diff --git a/faq/views/shallow.py b/faq/views/shallow.py
deleted file mode 100644
index 1a50852..0000000
--- a/faq/views/shallow.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from django.core.urlresolvers import reverse
-from django.shortcuts import get_object_or_404, redirect
-from django.views.generic.list_detail import object_list
-
-from faq.models import Topic, Question
-
-
-def _fragmentify(model, slug, url=None):
- get_object_or_404(model.objects.published().filter(slug=slug))
- url = url or reverse('faq-topic-list')
- fragment = '#%s' % slug
-
- return redirect(url + fragment, permanent=True)
-
-
-def topic_list(request):
- """
- A list view of all published Topics
-
- Templates:
- :template:`faq/topic_list.html`
- Context:
- topic_list
- A list of all published :model:`faq.Topic` objects that
- relate to the current :model:`sites.Site`.
-
- """
- return object_list(request, queryset=Topic.objects.published(),
- template_object_name='topic')
-
-
-def topic_detail(request, slug):
- """
- A detail view of a Topic
-
- Simply redirects to :view:`faq.views.topic_list` with the addition of
- a fragment identifier that links to the given :model:`faq.Topic`.
- E.g., ``/faq/#topic-slug``.
-
- """
- return _fragmentify(Topic, slug)
-
-
-def question_detail(request, topic_slug, slug):
- """
- A detail view of a Question.
-
- Simply redirects to :view:`faq.views.topic_list` with the addition of
- a fragment identifier that links to the given :model:`faq.Question`.
- E.g. ``/faq/#question-slug``.
-
- """
- return _fragmentify(Question, slug)
diff --git a/runtests.py b/runtests.py
new file mode 100644
index 0000000..920c331
--- /dev/null
+++ b/runtests.py
@@ -0,0 +1,73 @@
+import sys
+
+try:
+ from django.conf import settings
+
+ settings.configure(
+ DEBUG=True,
+ LANGUAGE_CODE='en-us',
+ USE_TZ=True,
+ DATABASES={
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': 'djangocms_youtube',
+ }
+ },
+ ROOT_URLCONF='faq.urls.normal',
+ INSTALLED_APPS=[
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sites',
+ 'faq',
+ ],
+ SITE_ID=1,
+ NOSE_ARGS=['-s'],
+ TEMPLATES=[{
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ "django.contrib.auth.context_processors.auth",
+ "django.template.context_processors.debug",
+ "django.template.context_processors.i18n",
+ "django.template.context_processors.media",
+ "django.template.context_processors.static",
+ "django.template.context_processors.tz",
+ "django.template.context_processors.request",
+ "django.contrib.messages.context_processors.messages",
+ ],
+ },
+ }, ]
+
+ )
+
+ try:
+ import django
+ setup = django.setup
+ except AttributeError:
+ pass
+ else:
+ setup()
+
+except ImportError:
+ import traceback
+ traceback.print_exc()
+ raise ImportError('To fix this error, run: pip install -r requirements-test.txt')
+
+
+def run_tests(*test_args):
+ from django.test.utils import get_runner
+ if not test_args:
+ test_args = ['faq.tests']
+
+ # Run tests
+ TestRunner = get_runner(settings) # NOQA
+ test_runner = TestRunner()
+ failures = test_runner.run_tests(test_args)
+
+ if failures:
+ sys.exit(failures)
+
+
+if __name__ == '__main__':
+ run_tests(*sys.argv[1:])
diff --git a/setup.py b/setup.py
index 73b859e..99207cb 100644
--- a/setup.py
+++ b/setup.py
@@ -1,42 +1,39 @@
# -*- coding: utf-8 -*-
-
import os
+import sys
-from distutils.core import setup
+from setuptools import setup, find_packages
here = os.path.dirname(__file__)
+version = __import__('faq').get_version()
+
def get_long_desc():
return open(os.path.join(here, 'README.rst')).read()
-# Function borrowed from carljm.
-def get_version():
- fh = open(os.path.join(here, "faq", "__init__.py"))
- try:
- for line in fh.readlines():
- if line.startswith("__version__ ="):
- return line.split("=")[1].strip().strip("'")
- finally:
- fh.close()
+if sys.argv[-1] == 'publish':
+ os.system('python setup.py bdist_wheel upload -r natgeo')
+ print("You probably want to also tag the version now:")
+ print(" python setup.py tag")
+ sys.exit()
+elif sys.argv[-1] == 'tag':
+ cmd = "git tag -a %s -m 'version %s';git push --tags" % (version, version)
+ os.system(cmd)
+ sys.exit()
+
setup(
name='django-faq',
- version=get_version(),
+ version=version.replace(' ', '-'),
description='Frequently Asked Question (FAQ) management for Django apps.',
- url='https://github.com/benspaulding/django-faq/',
+ url='https://github.com/natgeosociety/django-faq/',
author='Ben Spaulding',
author_email='ben@benspaulding.us',
license='BSD',
- download_url='http://github.com/benspaulding/django-faq/tarball/v%s' % get_version(),
- long_description = get_long_desc(),
- packages = [
- 'faq',
- 'faq.tests',
- 'faq.urls',
- 'faq.views',
- ],
- package_data = {
+ long_description=get_long_desc(),
+ packages=find_packages(exclude=['example*']),
+ package_data={
'faq': [
'fixtures/*',
'locale/*/LC_MESSAGES/*',
@@ -44,6 +41,7 @@ def get_version():
'templates/search/indexes/faq/*',
],
},
+ install_requires=['future'],
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..c7a8fb8
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,22 @@
+# Tox (https://tox.readthedocs.io/) is a tool for running tests
+# in multiple virtualenvs. This configuration file will run the
+# test suite on all supported python versions. To use it, "pip install tox"
+# and then run "tox" from this directory.
+
+[tox]
+envlist =
+ py{27,36}-django{19,110,111}
+ coverage-report
+
+[testenv]
+commands = coverage run --source faq runtests.py {posargs}
+deps =
+ coverage
+ django19: Django<1.10
+ django110: Django<1.11
+ django111: Django<2.0
+
+[testenv:coverage-report]
+commands =
+ coverage report -m
+ coverage xml