├── .gitignore ├── AUTHORS.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── dev-requirements.txt ├── example ├── __init__.py ├── manage.py ├── settings.py ├── templates │ └── home.html └── urls.py ├── faq ├── __init__.py ├── _testrunner.py ├── admin.py ├── faq ├── fixtures │ └── faq_test_data.json ├── forms.py ├── locale │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── pl_PL │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── managers.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ └── faq │ │ ├── base.html │ │ ├── question_detail.html │ │ ├── submit_question.html │ │ ├── submit_thanks.html │ │ ├── topic_detail.html │ │ └── topic_list.html ├── templatetags │ ├── __init__.py │ └── faqtags.py ├── tests │ ├── __init__.py │ ├── templates │ │ ├── 404.html │ │ └── 500.html │ ├── test_admin.py │ ├── test_models.py │ ├── test_templatetags.py │ └── test_views.py ├── urls.py └── views.py ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.bak 3 | *.db 4 | *.egg-info 5 | .tox/ 6 | .coverage 7 | htmlcov/ 8 | venv/ -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Django-FAQ was originally created by Kevin Fricovsky. 2 | 3 | Other contributors: 4 | 5 | * Brian Rosner 6 | * Greg Newman 7 | * Jacob Kaplan-Moss 8 | * Jannis Leidel 9 | * Justin Lilly 10 | * Peter Baumgartner 11 | * Rock Howard 12 | * Sean O'Connor 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Kevin Fricovsky and contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of this project nor the names of its contributors may 15 | be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.txt 2 | include LICENSE.txt 3 | include README.rst 4 | include MANIFEST.in 5 | recursive-include faq/locale * 6 | recursive-include faq/fixtures * 7 | recursive-include faq/templates * -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Django-FAQ 3 | ========== 4 | 5 | This is a simple FAQ application for a Django powered site. This app follows 6 | several "best practices" for reusable apps by allowing for template overrides 7 | and extra_context arguments and such. 8 | 9 | Features 10 | ======== 11 | 12 | Question Headers can be created that can be used to group related questions into 13 | sections. 14 | 15 | Questions can be "protected" in which case they are only presented to 16 | authenticated users. 17 | 18 | There are some saved FAQs in a fixture named ``initial_data.json`` that provide 19 | the example apps with some questions to view when you bring them up for the 20 | first time. These FAQs provide additional notes about installing and using 21 | django-faq. 22 | 23 | There is a ``SubmitFAQForm`` defined that you can use to allow site visitors to 24 | submit new questions and/or answers to the site administrator for consideration. 25 | All submitted questions are added as "inactive" and so it is up to the 26 | administrator to edit, activate or discard the question as well as set its' 27 | sort_order field and slug to reasonable values. 28 | 29 | Requirements 30 | ============ 31 | 32 | Django 1.3, Python 2.5 or greater. 33 | 34 | Installation 35 | ============ 36 | 37 | 1. ``pip install -e git://github.com/howiworkdaily/django-faq.git#egg=django_faq`` 38 | 39 | 2. Add ``"faq"`` to your ``INSTALLED_APPS`` setting. 40 | 41 | 3. Wire up the FAQ views by adding a line to your URLconf:: 42 | 43 | url('^faq/', include('faq.urls')) 44 | 45 | Note: do *not* use ``pip install django-faq`` to install this app, as that 46 | currently grabs another package entirely. 47 | 48 | If you want to customize the templates then either create an 'faq' directory in 49 | your projects templates location, or you can also pass along custom 50 | 'template_name' arguments by creating your own view wrappers around the 'faq' 51 | app views. I show how to do the latter in the 'example' project included - look 52 | at the views.py file to see the details. 53 | 54 | If you'd like to load some example data then run ``python manage.py loaddata 55 | faq_test_data.json`` 56 | 57 | Example Site 58 | ============ 59 | 60 | There is a stand-alone example site in the ``example`` directory. To 61 | try it out: 62 | 63 | 1. Install django-faq (see above). 64 | 65 | 2. Run ``python manage.py syncdb`` (This assumes that sqlite3 is available; if not 66 | you'll need to change the ``DATABASES`` setting first.) 67 | 68 | 3. If you'd like to load some example data then run 69 | ``python manage.py loaddata faq_test_data.json`` 70 | 71 | 4. Run ``python manage.py runserver`` and you will have the example site up and 72 | running. The home page will have links to get to the available views as well as 73 | to the admin. 74 | 75 | After logging into the admin you will notice an additional question appears in 76 | the FAQ. That question is "protected" and therefore not presented to 77 | non-authenticated users. 78 | 79 | The capability to submit an FAQ is available and works whether or not you are a 80 | logged in user. Note that a staff member will have to use the admin and review 81 | any submitted FAQs and clean them up and set them to active before they are 82 | viewable by the end user views. 83 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # Dependencies required to develop/test this app. 2 | 3 | coverage 4 | mock 5 | tox 6 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/howiworkdaily/django-faq/fc680d6be1deaa035e4bb2e752bb57db3eb0e096/example/__init__.py -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | try: 7 | import faq 8 | except ImportError: 9 | sys.stderr.write("django-faq isn't installed; trying to use a source checkout in ../faq.") 10 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 11 | 12 | from django.core.management import execute_manager 13 | try: 14 | import settings # Assumed to be in the same directory. 15 | except ImportError: 16 | import sys 17 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 18 | sys.exit(1) 19 | 20 | if __name__ == "__main__": 21 | execute_manager(settings) 22 | -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | # 2 | # A minimal settings file that ought to work out of the box for just about 3 | # anyone trying this project. It's deliberately missing most settings to keep 4 | # everything simple. 5 | # 6 | # A real app would have a lot more settings. The only important bit as far as 7 | # django-FAQ is concerned is to have `faq` in INSTALLED_APPS. 8 | # 9 | 10 | import os 11 | 12 | PROJECT_DIR = os.path.dirname(__file__) 13 | DEBUG = TEMPLATE_DEBUG = True 14 | 15 | DATABASES = { 16 | 'default': { 17 | 'ENGINE': 'django.db.backends.sqlite3', 18 | 'NAME': os.path.join(PROJECT_DIR, 'faq.db'), 19 | } 20 | } 21 | 22 | SITE_ID = 1 23 | SECRET_KEY = 'c#zi(mv^n+4te_sy$hpb*zdo7#f7ccmp9ro84yz9bmmfqj9y*c' 24 | ROOT_URLCONF = 'urls' 25 | TEMPLATE_DIRS = ( 26 | [os.path.join(PROJECT_DIR, "templates")] 27 | ) 28 | INSTALLED_APPS = ( 29 | 'django.contrib.auth', 30 | 'django.contrib.contenttypes', 31 | 'django.contrib.sessions', 32 | 'django.contrib.sites', 33 | 'django.contrib.admin', 34 | 35 | # Database migration helpers 36 | 'south', 37 | 38 | 'faq', 39 | ) -------------------------------------------------------------------------------- /example/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "faq/base.html" %} 2 | 3 | {% block body %} 4 | {% load url from future %} 5 |

Welcome to the django-faq example project

6 |

7 | If you aren't seeing any data below, or if you're getting random 404s, make 8 | sure you've loaded the sample data by running ./manage.py loaddata 9 | faq_test_data. 10 |

11 |

Here are some options to try:

12 |
13 |
The topic list.
14 |
This view shows a list of all the FAQ topic areas.
15 | 16 |
17 | A topic detail 18 | page. 19 |
20 |
21 | This shows the list of all questions in a given topic. 22 |
23 | 24 |
25 | A question 26 | detail page. 27 |
28 |
29 | This shows an individual question. Again, it'll only work if you've loaded 30 | the sample data. 31 |
32 | 33 |
34 | The question submission page. 35 |
36 |
37 | You can submit questions here. You'll notice that submitted questions 38 | don't immediately appear: they're set to "inactive" and have to be 39 | approved via the site admin. 40 |
41 | 42 |
43 | Speaking of the admin, here's the FAQ admin. 44 |
45 |
46 | Remember that you can clean up submitted questions in the admin before the 47 | will be visible in the FAQ. 48 |
49 | 50 |
51 | Finally, below are some questions fetched into this template using a 52 | template tag - {% templatetag openblock %} faq_list 3 as faqs 53 | {% templatetag closeblock %}. 54 |
55 |
56 | Some FAQ questions include: 57 | {% load faqtags %} 58 | {% faq_list 3 as faqs %} 59 | {% for faq in faqs %} 60 | {{ faq.text }} {% if not forloop.last %}/{% endif %} 61 | {% endfor %} 62 |
63 | 64 |
65 | {% endblock %} 66 | 67 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, url, include 2 | from django.contrib import admin; admin.autodiscover() 3 | from django.conf import settings 4 | from django.views.generic import TemplateView 5 | import faq.views 6 | 7 | urlpatterns = patterns('', 8 | # Just a simple example "home" page to show a bit of help/info. 9 | url(r'^$', TemplateView.as_view(template_name="home.html")), 10 | 11 | # This is the URLconf line you'd put in a real app to include the FAQ views. 12 | url(r'^faq/', include('faq.urls')), 13 | 14 | # Everybody wants an admin to wind a piece of string around. 15 | url(r'^admin/', include(admin.site.urls)), 16 | 17 | # Normally we'd do this if DEBUG only, but this is just an example app. 18 | url(regex = r'^static/(?P.*)$', 19 | view = 'django.views.static.serve', 20 | kwargs = {'document_root': settings.MEDIA_ROOT} 21 | ), 22 | ) -------------------------------------------------------------------------------- /faq/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/howiworkdaily/django-faq/fc680d6be1deaa035e4bb2e752bb57db3eb0e096/faq/__init__.py -------------------------------------------------------------------------------- /faq/_testrunner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test support harness to make setup.py test work. 3 | """ 4 | 5 | import sys 6 | 7 | from django.conf import settings 8 | settings.configure( 9 | DATABASES = { 10 | 'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory;'} 11 | }, 12 | INSTALLED_APPS = ['django.contrib.auth', 'django.contrib.contenttypes', 'faq'], 13 | ROOT_URLCONF = 'faq.urls', 14 | ) 15 | 16 | def runtests(): 17 | import django.test.utils 18 | runner_class = django.test.utils.get_runner(settings) 19 | test_runner = runner_class(verbosity=1, interactive=True) 20 | failures = test_runner.run_tests(['faq']) 21 | sys.exit(failures) -------------------------------------------------------------------------------- /faq/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from models import Question, Topic 3 | 4 | class TopicAdmin(admin.ModelAdmin): 5 | prepopulated_fields = {'slug':('name',)} 6 | 7 | class QuestionAdmin(admin.ModelAdmin): 8 | list_display = ['text', 'sort_order', 'created_by', 'created_on', 9 | 'updated_by', 'updated_on', 'status'] 10 | list_editable = ['sort_order', 'status'] 11 | 12 | def save_model(self, request, obj, form, change): 13 | ''' 14 | Update created-by / modified-by fields. 15 | 16 | The date fields are upadated at the model layer, but that's not got 17 | access to the user. 18 | ''' 19 | # If the object's new update the created_by field. 20 | if not change: 21 | obj.created_by = request.user 22 | 23 | # Either way update the updated_by field. 24 | obj.updated_by = request.user 25 | 26 | # Let the superclass do the final saving. 27 | return super(QuestionAdmin, self).save_model(request, obj, form, change) 28 | 29 | admin.site.register(Question, QuestionAdmin) 30 | admin.site.register(Topic, TopicAdmin) 31 | -------------------------------------------------------------------------------- /faq/faq: -------------------------------------------------------------------------------- 1 | faq -------------------------------------------------------------------------------- /faq/fixtures/faq_test_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "faq.topic", 5 | "fields": { 6 | "sort_order": 0, 7 | "name": "Silly questions", 8 | "slug": "silly-questions" 9 | } 10 | }, 11 | { 12 | "pk": 2, 13 | "model": "faq.topic", 14 | "fields": { 15 | "sort_order": 1, 16 | "name": "Serious questions", 17 | "slug": "serious-questions" 18 | } 19 | }, 20 | { 21 | "pk": 1, 22 | "model": "faq.question", 23 | "fields": { 24 | "status": 1, 25 | "updated_by": 1, 26 | "text": "What is your favorite color?", 27 | "created_by": 1, 28 | "topic": 1, 29 | "created_on": "2011-05-09 15:38:47", 30 | "sort_order": 0, 31 | "answer": "Red... no blue.. no *AHHHHH....*", 32 | "updated_on": "2011-05-09 15:38:47", 33 | "protected": false, 34 | "slug": "favorite-color" 35 | } 36 | }, 37 | { 38 | "pk": 2, 39 | "model": "faq.question", 40 | "fields": { 41 | "status": 1, 42 | "updated_by": 1, 43 | "text": "What is your quest?", 44 | "created_by": 1, 45 | "topic": 1, 46 | "created_on": "2011-05-09 15:39:19", 47 | "sort_order": 1, 48 | "answer": "I seek the grail!", 49 | "updated_on": "2011-05-09 15:39:19", 50 | "protected": false, 51 | "slug": "your-quest" 52 | } 53 | }, 54 | { 55 | "pk": 3, 56 | "model": "faq.question", 57 | "fields": { 58 | "status": 1, 59 | "updated_by": 1, 60 | "text": "What is Django-FAQ?", 61 | "created_by": 1, 62 | "topic": 2, 63 | "created_on": "2011-05-09 15:40:55", 64 | "sort_order": 3, 65 | "answer": "A simple FAQ application for a Django powered site. This app follows\r\nseveral \"best practices\" for reusable apps by allowing for template overrides\r\nand extra_context arguments and such.\r\n", 66 | "updated_on": "2011-05-09 15:40:55", 67 | "protected": false, 68 | "slug": "what-is-django-faq" 69 | } 70 | } 71 | ] -------------------------------------------------------------------------------- /faq/forms.py: -------------------------------------------------------------------------------- 1 | """ 2 | Here we define a form for allowing site users to submit a potential FAQ that 3 | they would like to see added. 4 | 5 | From the user's perspective the question is not added automatically, but 6 | actually it is, only it is added as inactive. 7 | """ 8 | 9 | from __future__ import absolute_import 10 | import datetime 11 | from django import forms 12 | from .models import Question, Topic 13 | 14 | class SubmitFAQForm(forms.ModelForm): 15 | class Meta: 16 | model = Question 17 | fields = ['topic', 'text', 'answer'] -------------------------------------------------------------------------------- /faq/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/howiworkdaily/django-faq/fc680d6be1deaa035e4bb2e752bb57db3eb0e096/faq/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /faq/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | #, fuzzy 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: PACKAGE VERSION\n" 5 | "Report-Msgid-Bugs-To: \n" 6 | "POT-Creation-Date: 2008-12-16 13:24+0100\n" 7 | "PO-Revision-Date: 2008-12-16 13:17+0100\n" 8 | "Last-Translator: Jannis Leidel \n" 9 | "Language-Team: German \n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | 14 | #: enums.py:10 15 | msgid "Active" 16 | msgstr "Aktiv" 17 | 18 | #: enums.py:11 19 | msgid "Inactive" 20 | msgstr "Inaktiv" 21 | 22 | #: models.py:12 23 | msgid "created by" 24 | msgstr "Autor" 25 | 26 | #: models.py:13 27 | msgid "created on" 28 | msgstr "Erstellt" 29 | 30 | #: models.py:14 31 | msgid "updated on" 32 | msgstr "Aktualisiert" 33 | 34 | #: models.py:15 35 | msgid "updated by" 36 | msgstr "Aktualisiert von" 37 | 38 | #: models.py:24 39 | msgid "slug" 40 | msgstr "Kürzel" 41 | 42 | #: models.py:24 43 | msgid "" 44 | "This is a unique identifier that allows your questions to display its detail " 45 | "view, ex 'how-can-i-contribute'" 46 | msgstr "" 47 | "Das ist ein eindeutiges Kürzel, das in der URL bei der Detaildarstellung von " 48 | "Fragen benutzt wird, z.B. 'how-can-i-contribute'" 49 | 50 | #: models.py:25 models.py:33 51 | msgid "question" 52 | msgstr "Frage" 53 | 54 | #: models.py:25 55 | msgid "The actual question itself." 56 | msgstr "Die eigentliche Frage." 57 | 58 | #: models.py:26 59 | msgid "answer" 60 | msgstr "Frage" 61 | 62 | #: models.py:26 63 | msgid "The answer text." 64 | msgstr "Der Antworttext." 65 | 66 | #: models.py:27 67 | msgid "status" 68 | msgstr "Status" 69 | 70 | #: models.py:27 71 | msgid "Only questions with their status set to 'Active' will be displayed." 72 | msgstr "Nur Fragen mit dem Status 'Aktiv ' werden angezeigt." 73 | 74 | #: models.py:28 75 | msgid "sort order" 76 | msgstr "Position" 77 | 78 | #: models.py:28 79 | msgid "The order you would like the question to be displayed." 80 | msgstr "Die Reihenfolge in der die Fragen dargestellt werden soll." 81 | 82 | #: models.py:34 83 | msgid "questions" 84 | msgstr "Fragen" 85 | -------------------------------------------------------------------------------- /faq/locale/pl_PL/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/howiworkdaily/django-faq/fc680d6be1deaa035e4bb2e752bb57db3eb0e096/faq/locale/pl_PL/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /faq/locale/pl_PL/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Django-faq Polish translation. 2 | # Copyright (C) 2012. 3 | # This file is distributed under the same license as the django-faq package. 4 | # Karol Majta , 2012. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: 0.1.0\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2012-06-23 23:08+0200\n" 11 | "PO-Revision-Date: 2012-06-23 23:08+0200\n" 12 | "Last-Translator: Karol Majta \n" 13 | "Language-Team: Polish \n" 14 | "Language: \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: models.py:12 20 | msgid "name" 21 | msgstr "imię" 22 | 23 | #: models.py:13 models.py:41 24 | msgid "slug" 25 | msgstr "slug" 26 | 27 | #: models.py:14 models.py:51 28 | msgid "sort order" 29 | msgstr "sortuj według" 30 | 31 | #: models.py:15 32 | msgid "The order you would like the topic to be displayed." 33 | msgstr "Wybierz jak posortowane będą tematy." 34 | 35 | #: models.py:21 36 | msgid "Topic" 37 | msgstr "Temat" 38 | 39 | #: models.py:22 40 | msgid "Topics" 41 | msgstr "Tematy" 42 | 43 | #: models.py:33 44 | msgid "Active" 45 | msgstr "Aktywny" 46 | 47 | #: models.py:34 48 | msgid "Inactive" 49 | msgstr "Nieaktywny" 50 | 51 | #: models.py:35 52 | msgid "Group Header" 53 | msgstr "Nagłówek Grupy" 54 | 55 | #: models.py:38 56 | msgid "question" 57 | msgstr "pytanie" 58 | 59 | #: models.py:38 60 | msgid "The actual question itself." 61 | msgstr "Treść pytania." 62 | 63 | #: models.py:39 64 | msgid "answer" 65 | msgstr "odpowiedź" 66 | 67 | #: models.py:39 68 | msgid "The answer text." 69 | msgstr "Treść odpowiedzi." 70 | 71 | #: models.py:40 72 | msgid "topic" 73 | msgstr "temat" 74 | 75 | #: models.py:42 76 | msgid "status" 77 | msgstr "status" 78 | 79 | #: models.py:44 80 | msgid "" 81 | "Only questions with their status set to 'Active' will be displayed. " 82 | "Questions marked as 'Group Header' are treated as such by views and " 83 | "templates that are set up to use them." 84 | msgstr "" 85 | "Jedynie pytania, których status ustawiono na 'Aktywny' będą wyświelane. " 86 | "Pytania oznaczone jako 'Nagłówek Grupy' są traktowane jako nagłówki przez " 87 | "widoki i szablony, które ich używają." 88 | 89 | #: models.py:48 90 | msgid "is protected" 91 | msgstr "chronione" 92 | 93 | #: models.py:49 94 | msgid "Set true if this question is only visible by authenticated users." 95 | msgstr "" 96 | "Zaznacz, jeśli to pytanie ma być widoczne jedynie dla zalogowanych " 97 | "użytkowników." 98 | 99 | #: models.py:52 100 | msgid "The order you would like the question to be displayed." 101 | msgstr "Wybierz jak posortowane będą pytania. " 102 | 103 | #: models.py:54 104 | msgid "created on" 105 | msgstr "utworzono" 106 | 107 | #: models.py:55 108 | msgid "updated on" 109 | msgstr "zmieniono" 110 | 111 | #: models.py:56 112 | msgid "created by" 113 | msgstr "utworzono" 114 | 115 | #: models.py:58 116 | msgid "updated by" 117 | msgstr "zmieniono" 118 | 119 | #: models.py:64 120 | msgid "Frequent asked question" 121 | msgstr "Często zadawane pytanie" 122 | 123 | #: models.py:65 124 | msgid "Frequently asked questions" 125 | msgstr "Często zadawane pytanie" 126 | 127 | #: views.py:95 128 | msgid "" 129 | "Your question was submitted and will be reviewed by for inclusion in the FAQ." 130 | msgstr "" 131 | "Twoje pytanie zostało przesłane. Dziękujemy, rozważymy umieszczenie go w FAQ." 132 | 133 | -------------------------------------------------------------------------------- /faq/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models.query import QuerySet 3 | 4 | class QuestionQuerySet(QuerySet): 5 | def active(self): 6 | """ 7 | Return only "active" (i.e. published) questions. 8 | """ 9 | return self.filter(status__exact=self.model.ACTIVE) 10 | 11 | class QuestionManager(models.Manager): 12 | def get_query_set(self): 13 | return QuestionQuerySet(self.model) 14 | 15 | def active(self): 16 | return self.get_query_set().active() -------------------------------------------------------------------------------- /faq/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'Topic' 12 | db.create_table(u'faq_topic', ( 13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('name', self.gf('django.db.models.fields.CharField')(max_length=150)), 15 | ('slug', self.gf('django.db.models.fields.SlugField')(max_length=150)), 16 | ('sort_order', self.gf('django.db.models.fields.IntegerField')(default=0)), 17 | )) 18 | db.send_create_signal(u'faq', ['Topic']) 19 | 20 | # Adding model 'Question' 21 | db.create_table(u'faq_question', ( 22 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 23 | ('text', self.gf('django.db.models.fields.TextField')()), 24 | ('answer', self.gf('django.db.models.fields.TextField')(blank=True)), 25 | ('topic', self.gf('django.db.models.fields.related.ForeignKey')(related_name='questions', to=orm['faq.Topic'])), 26 | ('slug', self.gf('django.db.models.fields.SlugField')(max_length=100)), 27 | ('status', self.gf('django.db.models.fields.IntegerField')(default=0)), 28 | ('protected', self.gf('django.db.models.fields.BooleanField')(default=False)), 29 | ('sort_order', self.gf('django.db.models.fields.IntegerField')(default=0)), 30 | ('created_on', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), 31 | ('updated_on', self.gf('django.db.models.fields.DateTimeField')()), 32 | ('created_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='+', null=True, to=orm['auth.User'])), 33 | ('updated_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='+', null=True, to=orm['auth.User'])), 34 | )) 35 | db.send_create_signal(u'faq', ['Question']) 36 | 37 | 38 | def backwards(self, orm): 39 | # Deleting model 'Topic' 40 | db.delete_table(u'faq_topic') 41 | 42 | # Deleting model 'Question' 43 | db.delete_table(u'faq_question') 44 | 45 | 46 | models = { 47 | u'auth.group': { 48 | 'Meta': {'object_name': 'Group'}, 49 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 50 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 51 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 52 | }, 53 | u'auth.permission': { 54 | 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, 55 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 57 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 58 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 59 | }, 60 | u'auth.user': { 61 | 'Meta': {'object_name': 'User'}, 62 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 63 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 64 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 65 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 66 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 67 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 68 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 69 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 70 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 71 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 72 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 73 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 74 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 75 | }, 76 | u'contenttypes.contenttype': { 77 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 78 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 79 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 80 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 81 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 82 | }, 83 | u'faq.question': { 84 | 'Meta': {'ordering': "['sort_order', 'created_on']", 'object_name': 'Question'}, 85 | 'answer': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 86 | 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': u"orm['auth.User']"}), 87 | 'created_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 88 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 89 | 'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 90 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100'}), 91 | 'sort_order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), 92 | 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}), 93 | 'text': ('django.db.models.fields.TextField', [], {}), 94 | 'topic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'questions'", 'to': u"orm['faq.Topic']"}), 95 | 'updated_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': u"orm['auth.User']"}), 96 | 'updated_on': ('django.db.models.fields.DateTimeField', [], {}) 97 | }, 98 | u'faq.topic': { 99 | 'Meta': {'ordering': "['sort_order', 'name']", 'object_name': 'Topic'}, 100 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 101 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), 102 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150'}), 103 | 'sort_order': ('django.db.models.fields.IntegerField', [], {'default': '0'}) 104 | } 105 | } 106 | 107 | complete_apps = ['faq'] -------------------------------------------------------------------------------- /faq/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/howiworkdaily/django-faq/fc680d6be1deaa035e4bb2e752bb57db3eb0e096/faq/migrations/__init__.py -------------------------------------------------------------------------------- /faq/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.db import models 3 | from django.utils.translation import ugettext_lazy as _ 4 | from django.contrib.auth import get_user_model 5 | from django.template.defaultfilters import slugify 6 | from managers import QuestionManager 7 | 8 | User = get_user_model() 9 | 10 | class Topic(models.Model): 11 | """ 12 | Generic Topics for FAQ question grouping 13 | """ 14 | name = models.CharField(_('name'), max_length=150) 15 | slug = models.SlugField(_('slug'), max_length=150) 16 | sort_order = models.IntegerField(_('sort order'), default=0, 17 | help_text=_('The order you would like the topic to be displayed.')) 18 | 19 | def get_absolute_url(self): 20 | return '/faq/' + self.slug 21 | 22 | class Meta: 23 | verbose_name = _("Topic") 24 | verbose_name_plural = _("Topics") 25 | ordering = ['sort_order', 'name'] 26 | 27 | def __unicode__(self): 28 | return self.name 29 | 30 | class Question(models.Model): 31 | HEADER = 2 32 | ACTIVE = 1 33 | INACTIVE = 0 34 | STATUS_CHOICES = ( 35 | (ACTIVE, _('Active')), 36 | (INACTIVE, _('Inactive')), 37 | (HEADER, _('Group Header')), 38 | ) 39 | 40 | text = models.TextField(_('question'), help_text=_('The actual question itself.')) 41 | answer = models.TextField(_('answer'), blank=True, help_text=_('The answer text.')) 42 | topic = models.ForeignKey(Topic, verbose_name=_('topic'), related_name='questions') 43 | slug = models.SlugField(_('slug'), max_length=100) 44 | status = models.IntegerField(_('status'), 45 | choices=STATUS_CHOICES, default=INACTIVE, 46 | help_text=_("Only questions with their status set to 'Active' will be " 47 | "displayed. Questions marked as 'Group Header' are treated " 48 | "as such by views and templates that are set up to use them.")) 49 | 50 | protected = models.BooleanField(_('is protected'), default=False, 51 | help_text=_("Set true if this question is only visible by authenticated users.")) 52 | 53 | sort_order = models.IntegerField(_('sort order'), default=0, 54 | help_text=_('The order you would like the question to be displayed.')) 55 | 56 | created_on = models.DateTimeField(_('created on'), default=datetime.datetime.now) 57 | updated_on = models.DateTimeField(_('updated on')) 58 | created_by = models.ForeignKey(User, verbose_name=_('created by'), 59 | null=True, related_name="+") 60 | updated_by = models.ForeignKey(User, verbose_name=_('updated by'), 61 | null=True, related_name="+") 62 | 63 | objects = QuestionManager() 64 | 65 | class Meta: 66 | verbose_name = _("Frequent asked question") 67 | verbose_name_plural = _("Frequently asked questions") 68 | ordering = ['sort_order', 'created_on'] 69 | 70 | def __unicode__(self): 71 | return self.text 72 | 73 | def save(self, *args, **kwargs): 74 | # Set the date updated. 75 | self.updated_on = datetime.datetime.now() 76 | 77 | # Create a unique slug, if needed. 78 | if not self.slug: 79 | suffix = 0 80 | potential = base = slugify(self.text[:90]) 81 | while not self.slug: 82 | if suffix: 83 | potential = "%s-%s" % (base, suffix) 84 | if not Question.objects.filter(slug=potential).exists(): 85 | self.slug = potential 86 | # We hit a conflicting slug; increment the suffix and try again. 87 | suffix += 1 88 | 89 | super(Question, self).save(*args, **kwargs) 90 | 91 | def is_header(self): 92 | return self.status == Question.HEADER 93 | 94 | def is_active(self): 95 | return self.status == Question.ACTIVE 96 | -------------------------------------------------------------------------------- /faq/templates/faq/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Django FAQ {% endblock %} 6 | 7 | 8 | {% for message in messages %} 9 |

{{ message }}

10 | {% endfor %} 11 | {% block body %}{% endblock %} 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /faq/templates/faq/question_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "faq/base.html" %} 2 | 3 | {% block title %}{{ block.super }}: Question - {{ object.text|truncatewords:30}}{% endblock %} 4 | 5 | 6 | {% block body %} 7 | 8 | {{ object.text }} 9 | 10 |
11 | 12 | {{ object.answer }} 13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /faq/templates/faq/submit_question.html: -------------------------------------------------------------------------------- 1 | {% extends "faq/base.html" %} 2 | 3 | {% block title %}{{ block.super }}: Add Question{% endblock %} 4 | 5 | {% block body %} 6 |

Submit Question

7 |

8 | Use this form to submit a question (and optionally a corresponding answer) 9 | that you would like to see added to the FAQs on this site. 10 |

11 |
12 | {% csrf_token %} 13 | 14 | {{ form.as_table }} 15 |
16 | 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /faq/templates/faq/submit_thanks.html: -------------------------------------------------------------------------------- 1 | {% extends "faq/base.html" %} 2 | 3 | {% block title %}{{ block.super }}: Thanks{% endblock %} 4 | {% block body %}

Thanks!

{% endblock %} 5 | -------------------------------------------------------------------------------- /faq/templates/faq/topic_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "faq/base.html" %} 2 | 3 | {% block title %}{{ block.super }}: {{ topic }}{% endblock %} 4 | 5 | {% block body %} 6 |

{{ topic }}

7 |
8 | {% for question in questions %} 9 |
{{ question.text }}
10 |
{{ question.answer }}
11 | {% endfor %} 12 |
13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /faq/templates/faq/topic_list.html: -------------------------------------------------------------------------------- 1 | {% extends "faq/base.html" %} 2 | 3 | {% block title %}{{ block.super }}: Topics{% endblock %} 4 | 5 | {% block body %} 6 |

Topics

7 |
    8 | {% for topic in topics %} 9 |
  • {{ topic }}
  • 10 | {% endfor %} 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /faq/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/howiworkdaily/django-faq/fc680d6be1deaa035e4bb2e752bb57db3eb0e096/faq/templatetags/__init__.py -------------------------------------------------------------------------------- /faq/templatetags/faqtags.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from django import template 4 | from ..models import Question, Topic 5 | 6 | register = template.Library() 7 | 8 | class FaqListNode(template.Node): 9 | def __init__(self, num, varname, topic=None): 10 | self.num = template.Variable(num) 11 | self.topic = template.Variable(topic) if topic else None 12 | self.varname = varname 13 | 14 | def render(self, context): 15 | try: 16 | num = self.num.resolve(context) 17 | topic = self.topic.resolve(context) if self.topic else None 18 | except template.VariableDoesNotExist: 19 | return '' 20 | 21 | if isinstance(topic, Topic): 22 | qs = Question.objects.filter(topic=topic) 23 | elif topic is not None: 24 | qs = Question.objects.filter(topic__slug=topic) 25 | else: 26 | qs = Question.objects.all() 27 | 28 | context[self.varname] = qs.filter(status=Question.ACTIVE)[:num] 29 | return '' 30 | 31 | @register.tag 32 | def faqs_for_topic(parser, token): 33 | """ 34 | Returns a list of 'count' faq's that belong to the given topic 35 | the supplied topic argument must be in the slug format 'topic-name' 36 | 37 | Example usage:: 38 | 39 | {% faqs_for_topic 5 "my-slug" as faqs %} 40 | """ 41 | 42 | args = token.split_contents() 43 | if len(args) != 5: 44 | raise template.TemplateSyntaxError("%s takes exactly four arguments" % args[0]) 45 | if args[3] != 'as': 46 | raise template.TemplateSyntaxError("third argument to the %s tag must be 'as'" % args[0]) 47 | 48 | return FaqListNode(num=args[1], topic=args[2], varname=args[4]) 49 | 50 | 51 | @register.tag 52 | def faq_list(parser, token): 53 | """ 54 | returns a generic list of 'count' faq's to display in a list 55 | ordered by the faq sort order. 56 | 57 | Example usage:: 58 | 59 | {% faq_list 15 as faqs %} 60 | """ 61 | args = token.split_contents() 62 | if len(args) != 4: 63 | raise template.TemplateSyntaxError("%s takes exactly three arguments" % args[0]) 64 | if args[2] != 'as': 65 | raise template.TemplateSyntaxError("second argument to the %s tag must be 'as'" % args[0]) 66 | 67 | return FaqListNode(num=args[1], varname=args[3]) 68 | -------------------------------------------------------------------------------- /faq/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from faq.tests.test_admin import * 2 | from faq.tests.test_models import * 3 | from faq.tests.test_templatetags import * 4 | from faq.tests.test_views import * 5 | -------------------------------------------------------------------------------- /faq/tests/templates/404.html: -------------------------------------------------------------------------------- 1 | 404 -------------------------------------------------------------------------------- /faq/tests/templates/500.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/howiworkdaily/django-faq/fc680d6be1deaa035e4bb2e752bb57db3eb0e096/faq/tests/templates/500.html -------------------------------------------------------------------------------- /faq/tests/test_admin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Some basic admin tests. 3 | 4 | Rather than testing the frontend UI -- that's be a job for something like 5 | Selenium -- this does a bunch of mocking and just tests the various admin 6 | callbacks. 7 | """ 8 | 9 | from __future__ import absolute_import 10 | 11 | import mock 12 | from django.contrib import admin 13 | from django.contrib.auth.models import User 14 | from django.utils import unittest 15 | from django.http import HttpRequest 16 | from django import forms 17 | from ..admin import QuestionAdmin 18 | from ..models import Question 19 | 20 | class FAQAdminTests(unittest.TestCase): 21 | 22 | def test_question_admin_save_model(self): 23 | user1 = mock.Mock(spec=User) 24 | user2 = mock.Mock(spec=User) 25 | req = mock.Mock(spec=HttpRequest) 26 | obj = mock.Mock(spec=Question) 27 | form = mock.Mock(spec=forms.Form) 28 | 29 | qa = QuestionAdmin(Question, admin.site) 30 | 31 | # Test saving a new model. 32 | req.user = user1 33 | qa.save_model(req, obj, form, change=False) 34 | obj.save.assert_called() 35 | self.assertEqual(obj.created_by, user1, "created_by wasn't set to request.user") 36 | self.assertEqual(obj.updated_by, user1, "updated_by wasn't set to request.user") 37 | 38 | # And saving an existing model. 39 | obj.save.reset_mock() 40 | req.user = user2 41 | qa.save_model(req, obj, form, change=True) 42 | obj.save.assert_called() 43 | self.assertEqual(obj.created_by, user1, "created_by shouldn't have been changed") 44 | self.assertEqual(obj.updated_by, user2, "updated_by wasn't set to request.user") -------------------------------------------------------------------------------- /faq/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import datetime 4 | import django.test 5 | from ..models import Topic, Question 6 | 7 | class FAQModelTests(django.test.TestCase): 8 | 9 | def test_model_save(self): 10 | t = Topic.objects.create(name='t', slug='t') 11 | q = Question.objects.create( 12 | text = "What is your quest?", 13 | answer = "I see the grail!", 14 | topic = t 15 | ) 16 | self.assertEqual(q.created_on.date(), datetime.date.today()) 17 | self.assertEqual(q.updated_on.date(), datetime.date.today()) 18 | self.assertEqual(q.slug, "what-is-your-quest") 19 | 20 | def test_model_save_duplicate_slugs(self): 21 | t = Topic.objects.create(name='t', slug='t') 22 | q = Question.objects.create( 23 | text = "What is your quest?", 24 | answer = "I see the grail!", 25 | topic = t 26 | ) 27 | q2 = Question.objects.create( 28 | text = "What is your quest?", 29 | answer = "I see the grail!", 30 | topic = t 31 | ) 32 | self.assertEqual(q2.slug, 'what-is-your-quest-1') -------------------------------------------------------------------------------- /faq/tests/test_templatetags.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import django.test 4 | from django import template 5 | from django.utils import unittest 6 | from ..templatetags import faqtags 7 | from ..models import Topic 8 | 9 | class FAQTagsSyntaxTests(unittest.TestCase): 10 | """ 11 | Tests for the syntax/compliation functions. 12 | 13 | These are broken out here so that they don't have to be 14 | django.test.TestCases, which are slower. 15 | """ 16 | 17 | def compile(self, tagfunc, token_contents): 18 | """ 19 | Mock out a call to a template compliation function. 20 | 21 | Assumes the tag doesn't use the parser, so this won't work for block tags. 22 | """ 23 | t = template.Token(template.TOKEN_BLOCK, token_contents) 24 | return tagfunc(None, t) 25 | 26 | def test_faqs_for_topic_compile(self): 27 | t = self.compile(faqtags.faqs_for_topic, "faqs_for_topic 15 'some-slug' as faqs") 28 | self.assertEqual(t.num.var, "15") 29 | self.assertEqual(t.topic.var, "'some-slug'") 30 | self.assertEqual(t.varname, "faqs") 31 | 32 | def test_faqs_for_topic_too_few_arguments(self): 33 | self.assertRaises(template.TemplateSyntaxError, 34 | self.compile, 35 | faqtags.faqs_for_topic, 36 | "faqs_for_topic 15 'some-slug' as") 37 | 38 | def test_faqs_for_topic_too_many_arguments(self): 39 | self.assertRaises(template.TemplateSyntaxError, 40 | self.compile, 41 | faqtags.faqs_for_topic, 42 | "faqs_for_topic 15 'some-slug' as varname foobar") 43 | 44 | def test_faqs_for_topic_bad_as(self): 45 | self.assertRaises(template.TemplateSyntaxError, 46 | self.compile, 47 | faqtags.faqs_for_topic, 48 | "faqs_for_topic 15 'some-slug' blahblah varname") 49 | 50 | def test_faq_list_compile(self): 51 | t = self.compile(faqtags.faq_list, "faq_list 15 as faqs") 52 | self.assertEqual(t.num.var, "15") 53 | self.assertEqual(t.varname, "faqs") 54 | 55 | def test_faq_list_too_few_arguments(self): 56 | self.assertRaises(template.TemplateSyntaxError, 57 | self.compile, 58 | faqtags.faq_list, 59 | "faq_list 15") 60 | 61 | def test_faq_list_too_many_arguments(self): 62 | self.assertRaises(template.TemplateSyntaxError, 63 | self.compile, 64 | faqtags.faq_list, 65 | "faq_list 15 as varname foobar") 66 | 67 | def test_faq_list_bad_as(self): 68 | self.assertRaises(template.TemplateSyntaxError, 69 | self.compile, 70 | faqtags.faq_list, 71 | "faq_list 15 blahblah varname") 72 | 73 | class FAQTagsNodeTests(django.test.TestCase): 74 | """ 75 | Tests for the node classes themselves, and hence the rendering functions. 76 | """ 77 | fixtures = ['faq_test_data.json'] 78 | 79 | def test_faqs_for_topic_node(self): 80 | context = template.Context() 81 | node = faqtags.FaqListNode(num='5', topic='"silly-questions"', varname="faqs") 82 | content = node.render(context) 83 | self.assertEqual(content, "") 84 | self.assertQuerysetEqual(context['faqs'], 85 | ['', 86 | '']) 87 | 88 | def test_faqs_for_topic_node_variable_arguments(self): 89 | """ 90 | Test faqs_for_topic with a variable arguments. 91 | """ 92 | context = template.Context({'topic': Topic.objects.get(pk=1), 93 | 'number': 1}) 94 | node = faqtags.FaqListNode(num='number', topic='topic', varname="faqs") 95 | content = node.render(context) 96 | self.assertEqual(content, "") 97 | self.assertQuerysetEqual(context['faqs'], [""]) 98 | 99 | def test_faqs_for_topic_node_invalid_variables(self): 100 | context = template.Context() 101 | node = faqtags.FaqListNode(num='number', topic='topic', varname="faqs") 102 | content = node.render(context) 103 | self.assertEqual(content, "") 104 | self.assert_("faqs" not in context, 105 | "faqs variable shouldn't have been added to the context.") 106 | 107 | def test_faq_list_node(self): 108 | context = template.Context() 109 | node = faqtags.FaqListNode(num='5', varname="faqs") 110 | content = node.render(context) 111 | self.assertEqual(content, "") 112 | self.assertQuerysetEqual(context['faqs'], 113 | ['', 114 | '', 115 | '']) 116 | 117 | def test_faq_list_node_variable_arguments(self): 118 | """ 119 | Test faqs_for_topic with a variable arguments. 120 | """ 121 | context = template.Context({'topic': Topic.objects.get(pk=1), 122 | 'number': 1}) 123 | node = faqtags.FaqListNode(num='number', varname="faqs") 124 | content = node.render(context) 125 | self.assertEqual(content, "") 126 | self.assertQuerysetEqual(context['faqs'], [""]) 127 | 128 | def test_faq_list_node_invalid_variables(self): 129 | context = template.Context() 130 | node = faqtags.FaqListNode(num='number', varname="faqs") 131 | content = node.render(context) 132 | self.assertEqual(content, "") 133 | self.assert_("faqs" not in context, 134 | "faqs variable shouldn't have been added to the context.") 135 | 136 | -------------------------------------------------------------------------------- /faq/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import datetime 4 | import django.test 5 | import mock 6 | import os 7 | from django.conf import settings 8 | from ..models import Topic, Question 9 | 10 | class FAQViewTests(django.test.TestCase): 11 | urls = 'faq.urls' 12 | fixtures = ['faq_test_data.json'] 13 | 14 | def setUp(self): 15 | # Make some test templates available. 16 | self._oldtd = settings.TEMPLATE_DIRS 17 | settings.TEMPLATE_DIRS = [os.path.join(os.path.dirname(__file__), 'templates')] 18 | 19 | def tearDown(self): 20 | settings.TEMPLATE_DIRS = self._oldtd 21 | 22 | def test_submit_faq_get(self): 23 | response = self.client.get('/submit/') 24 | self.assertEqual(response.status_code, 200) 25 | self.assertTemplateUsed(response, "faq/submit_question.html") 26 | 27 | @mock.patch('django.contrib.messages') 28 | def test_submit_faq_post(self, mock_messages): 29 | data = { 30 | 'topic': '1', 31 | 'text': 'What is your favorite color?', 32 | 'answer': 'Blue. I mean red. I mean *AAAAHHHHH....*', 33 | } 34 | response = self.client.post('/submit/', data) 35 | mock_messages.sucess.assert_called() 36 | self.assertRedirects(response, "/submit/thanks/") 37 | self.assert_( 38 | Question.objects.filter(text=data['text']).exists(), 39 | "Expected question object wasn't created." 40 | ) 41 | 42 | def test_submit_thanks(self): 43 | response = self.client.get('/submit/thanks/') 44 | self.assertEqual(response.status_code, 200) 45 | self.assertTemplateUsed(response, "faq/submit_thanks.html") 46 | 47 | def test_faq_index(self): 48 | response = self.client.get('/') 49 | self.assertEqual(response.status_code, 200) 50 | self.assertTemplateUsed(response, "faq/topic_list.html") 51 | self.assertQuerysetEqual( 52 | response.context["topics"], 53 | ["", ""] 54 | ) 55 | self.assertEqual( 56 | response.context['last_updated'], 57 | Question.objects.order_by('-updated_on')[0].updated_on 58 | ) 59 | 60 | def test_topic_detail(self): 61 | response = self.client.get('/silly-questions/') 62 | self.assertEqual(response.status_code, 200) 63 | self.assertTemplateUsed(response, "faq/topic_detail.html") 64 | self.assertEqual( 65 | response.context['topic'], 66 | Topic.objects.get(slug="silly-questions") 67 | ) 68 | self.assertEqual( 69 | response.context['last_updated'], 70 | Topic.objects.get(slug='silly-questions').questions.order_by('-updated_on')[0].updated_on 71 | ) 72 | self.assertQuerysetEqual( 73 | response.context["questions"], 74 | ["", 75 | ""] 76 | ) 77 | 78 | def test_question_detail(self): 79 | response = self.client.get('/silly-questions/your-quest/') 80 | self.assertEqual(response.status_code, 200) 81 | self.assertTemplateUsed(response, "faq/question_detail.html") 82 | self.assertEqual( 83 | response.context["question"], 84 | Question.objects.get(slug="your-quest") 85 | ) 86 | -------------------------------------------------------------------------------- /faq/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from django.conf.urls.defaults import * 4 | from . import views as faq_views 5 | 6 | urlpatterns = patterns('', 7 | url(regex = r'^$', 8 | view = faq_views.TopicList.as_view(), 9 | name = 'faq_topic_list', 10 | ), 11 | url(regex = r'^submit/$', 12 | view = faq_views.SubmitFAQ.as_view(), 13 | name = 'faq_submit', 14 | ), 15 | url(regex = r'^submit/thanks/$', 16 | view = faq_views.SubmitFAQThanks.as_view(), 17 | name = 'faq_submit_thanks', 18 | ), 19 | url(regex = r'^(?P[\w-]+)/$', 20 | view = faq_views.TopicDetail.as_view(), 21 | name = 'faq_topic_detail', 22 | ), 23 | url(regex = r'^(?P[\w-]+)/(?P[\w-]+)/$', 24 | view = faq_views.QuestionDetail.as_view(), 25 | name = 'faq_question_detail', 26 | ), 27 | ) -------------------------------------------------------------------------------- /faq/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django.db.models import Max 3 | from django.core.urlresolvers import reverse, NoReverseMatch 4 | from django.contrib import messages 5 | from django.http import Http404 6 | from django.shortcuts import redirect, render, get_object_or_404 7 | from django.utils.translation import ugettext as _ 8 | from django.views.generic import ListView, DetailView, TemplateView, CreateView 9 | from .models import Question, Topic 10 | from .forms import SubmitFAQForm 11 | 12 | class TopicList(ListView): 13 | model = Topic 14 | template = "faq/topic_list.html" 15 | allow_empty = True 16 | context_object_name = "topics" 17 | 18 | def get_context_data(self, **kwargs): 19 | data = super(TopicList, self).get_context_data(**kwargs) 20 | 21 | # This slightly magical queryset grabs the latest update date for 22 | # topic's questions, then the latest date for that whole group. 23 | # In other words, it's:: 24 | # 25 | # max(max(q.updated_on for q in topic.questions) for topic in topics) 26 | # 27 | # Except performed in the DB, so quite a bit more efficiant. 28 | # 29 | # We can't just do Question.objects.all().aggregate(max('updated_on')) 30 | # because that'd prevent a subclass from changing the view's queryset 31 | # (or even model -- this view'll even work with a different model 32 | # as long as that model has a many-to-one to something called "questions" 33 | # with an "updated_on" field). So this magic is the price we pay for 34 | # being generic. 35 | last_updated = (data['object_list'] 36 | .annotate(updated=Max('questions__updated_on')) 37 | .aggregate(Max('updated'))) 38 | 39 | data.update({'last_updated': last_updated['updated__max']}) 40 | return data 41 | 42 | class TopicDetail(DetailView): 43 | model = Topic 44 | template = "faq/topic_detail.html" 45 | context_object_name = "topic" 46 | 47 | def get_context_data(self, **kwargs): 48 | # Include a list of questions this user has access to. If the user is 49 | # logged in, this includes protected questions. Otherwise, not. 50 | qs = self.object.questions.active() 51 | if self.request.user.is_anonymous(): 52 | qs = qs.exclude(protected=True) 53 | 54 | data = super(TopicDetail, self).get_context_data(**kwargs) 55 | data.update({ 56 | 'questions': qs, 57 | 'last_updated': qs.aggregate(updated=Max('updated_on'))['updated'], 58 | }) 59 | return data 60 | 61 | class QuestionDetail(DetailView): 62 | queryset = Question.objects.active() 63 | template = "faq/question_detail.html" 64 | 65 | def get_queryset(self): 66 | topic = get_object_or_404(Topic, slug=self.kwargs['topic_slug']) 67 | 68 | # Careful here not to hardcode a base queryset. This lets 69 | # subclassing users re-use this view on a subset of questions, or 70 | # even on a new model. 71 | # FIXME: similar logic as above. This should push down into managers. 72 | qs = super(QuestionDetail, self).get_queryset().filter(topic=topic) 73 | if self.request.user.is_anonymous(): 74 | qs = qs.exclude(protected=True) 75 | 76 | return qs 77 | 78 | class SubmitFAQ(CreateView): 79 | model = Question 80 | form_class = SubmitFAQForm 81 | template_name = "faq/submit_question.html" 82 | success_view_name = "faq_submit_thanks" 83 | 84 | def get_form_kwargs(self): 85 | kwargs = super(SubmitFAQ, self).get_form_kwargs() 86 | kwargs['instance'] = Question() 87 | if self.request.user.is_authenticated(): 88 | kwargs['instance'].created_by = self.request.user 89 | return kwargs 90 | 91 | def form_valid(self, form): 92 | response = super(SubmitFAQ, self).form_valid(form) 93 | messages.success(self.request, 94 | _("Your question was submitted and will be reviewed by for inclusion in the FAQ."), 95 | fail_silently=True, 96 | ) 97 | return response 98 | 99 | def get_success_url(self): 100 | # The superclass version raises ImproperlyConfigered if self.success_url 101 | # isn't set. Instead of that, we'll try to redirect to a named view. 102 | if self.success_url: 103 | return self.success_url 104 | else: 105 | return reverse(self.success_view_name) 106 | 107 | class SubmitFAQThanks(TemplateView): 108 | template_name = "faq/submit_thanks.html" -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | def read(fname): 5 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 6 | 7 | setup( 8 | name = 'django-faq', 9 | version = '0.1.0', 10 | description = 'A simple FAQ application for Django sites.', 11 | long_description = read('README.rst'), 12 | 13 | author ='Kevin Fricovsky', 14 | author_email = 'kfricovsky@gmail.com', 15 | url = 'http://github.com/howiworkdaily/django-faq', 16 | 17 | packages = find_packages(exclude=['example']), 18 | zip_safe = False, 19 | 20 | classifiers = [ 21 | 'Development Status :: 3 - Alpha', 22 | 'Environment :: Web Environment', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: BSD License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | 'Framework :: Django', 28 | ], 29 | 30 | install_requires = ['setuptools', 'Django >= 1.3'], 31 | test_suite = "faq._testrunner.runtests" 32 | ) 33 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | downloadcache = .tox/_download/ 3 | envlist = py25, py26, py27 4 | 5 | [testenv] 6 | deps = 7 | mock 8 | commands = 9 | {envpython} setup.py test 10 | 11 | # There's no need to measure coverage on each different pyversion (I think!) 12 | # so only do it for 2.7 (chosen arbitrarily). 13 | [testenv:py27] 14 | deps = 15 | coverage 16 | mock 17 | commands = 18 | coverage run --branch --source=faq setup.py test 19 | coverage report --omit=faq/_testrunner.py,faq/tests/* 20 | coverage html --omit=faq/_testrunner.py,faq/tests/* -d htmlcov/ 21 | --------------------------------------------------------------------------------