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