├── .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 |
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 |
--------------------------------------------------------------------------------