├── .github
├── dependabot.yml
└── workflows
│ ├── codeql-analysis.yml
│ ├── python-publish.yml
│ └── test.yml
├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.md
├── __init__.py
├── demo
├── default-1.png
├── default-2.png
├── default-3.png
├── default-4.png
├── demo-templates
│ ├── answer_form.html
│ ├── base.html
│ ├── categories_list.html
│ ├── category_detail.html
│ ├── comment_form.html
│ ├── comments.html
│ ├── question_base.html
│ ├── question_detail.html
│ ├── question_form.html
│ ├── questions_list.html
│ └── vote_form.html
├── demo.md
├── option-1-1.png
├── option-1-2.png
├── option-10-1.png
├── option-11-1.png
├── option-2-1.png
├── option-2-2.png
├── option-3-1.png
├── option-3-2.png
├── option-4-1.png
├── option-4-2.png
├── option-5-1.png
├── option-6-1.png
├── option-8-1.png
└── option-9-1.png
├── example
├── example
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── home
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── templates
│ │ └── home
│ │ │ └── index.html
│ ├── tests.py
│ └── views.py
├── manage.py
├── requirements.txt
└── templates
│ └── faq
│ ├── answer_form.html
│ ├── base.html
│ ├── categories_list.html
│ ├── category_detail.html
│ ├── comment_form.html
│ ├── comments.html
│ ├── question_base.html
│ ├── question_detail.html
│ ├── question_form.html
│ ├── questions_list.html
│ └── vote_form.html
├── faq
├── __init__.py
├── admin.py
├── apps.py
├── forms.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_alter_answer_id_alter_answerhelpful_id_and_more.py
│ ├── 0003_auto_20220619_0939.py
│ ├── 0004_alter_answer_slug_alter_category_slug.py
│ ├── 0005_rename_description_category__description.py
│ ├── 0006_answer_is_rich_text.py
│ └── __init__.py
├── models.py
├── path_converters.py
├── snippets.py
├── templates
│ └── faq
│ │ ├── answer_form.html
│ │ ├── base.html
│ │ ├── categories_list.html
│ │ ├── category_detail.html
│ │ ├── comment_form.html
│ │ ├── comments.html
│ │ ├── question_base.html
│ │ ├── question_detail.html
│ │ ├── question_form.html
│ │ ├── questions_list.html
│ │ └── vote_form.html
├── tests.py
├── urls.py
└── views.py
└── setup.py
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "pip" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '27 8 * * 0'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'python' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37 | # Learn more:
38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
39 |
40 | steps:
41 | - name: Checkout repository
42 | uses: actions/checkout@v2
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Python 🐍 distribution 📦 to PyPI
2 |
3 | on:
4 | release:
5 | types:
6 | - created
7 | jobs:
8 | build:
9 | name: Build distribution 📦
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Set up Python
15 | uses: actions/setup-python@v4
16 | with:
17 | python-version: "3.x"
18 | - name: Set env
19 | run:
20 | echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
21 | - name: Install pypa/build
22 | run: >-
23 | python3 -m
24 | pip install
25 | build
26 | --user
27 | - name: Build a binary wheel and a source tarball
28 | run: python3 -m build
29 | - name: Store the distribution packages
30 | uses: actions/upload-artifact@v3
31 | with:
32 | name: python-package-distributions
33 | path: dist/
34 | publish-to-pypi:
35 | name: >-
36 | Publish Python 🐍 distribution 📦 to PyPI
37 | needs:
38 | - build
39 | runs-on: ubuntu-latest
40 | environment:
41 | name: pypi
42 | url: https://pypi.org/p/django-easy-faq/
43 | permissions:
44 | id-token: write # IMPORTANT: mandatory for trusted publishing
45 | steps:
46 | - name: Download all the dists
47 | uses: actions/download-artifact@v3
48 | with:
49 | name: python-package-distributions
50 | path: dist/
51 | - name: Publish distribution 📦 to PyPI
52 | uses: pypa/gh-action-pypi-publish@release/v1
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test django-easy-faq
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | max-parallel: 4
14 | matrix:
15 | python-version: ["3.9", "3.12"]
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Set up Python ${{ matrix.python-version }}
20 | uses: actions/setup-python@v3
21 | with:
22 | python-version: ${{ matrix.python-version }}
23 | - name: Set env
24 | run: echo "RELEASE_VERSION=0.0.1" >> $GITHUB_ENV
25 | - name: Install Dependencies
26 | run: |
27 | pip install django-tinymce
28 | pip install Django
29 | pip install -e .
30 | - name: Run Tests
31 | run: |
32 | cd example
33 | python manage.py test faq
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | __pycache__/__init__.cpython-38.pyc
3 | *.pyc
4 | /.idea/.gitignore
5 | /.idea/django-easy-faq.iml
6 | /dist/django-easy-faq-1.4.tar.gz
7 | /.idea/misc.xml
8 | /.idea/modules.xml
9 | /.idea/inspectionProfiles/profiles_settings.xml
10 | /.idea/vcs.xml
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 smark-1
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include faq/migrations *
2 | recursive-include faq/templates *
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # django-easy-faq
2 |
3 | django-easy-faq is a Django app to allow for a simple yet feature rich faq app. with categories, commenting voting of questions and answers all as an optional part of the app. To see screenshots of what this django-easy-faq could look like with bootstrap 5 styling [click here](demo/demo.md).
4 |
5 |
6 | ## Quick start
7 |
8 | 1. pip install:
9 |
10 | `pip install django-easy-faq`
11 |
12 | 2. Add "faq" to your INSTALLED_APPS setting like this:
13 |
14 | ```python
15 | INSTALLED_APPS = [
16 | ...
17 | 'faq',]
18 | ```
19 |
20 | 3. Include the easy-faq URLconf in your project urls.py like this::
21 |
22 | ```python
23 | #…
24 | path('faq/', include('faq.urls')),
25 | #…
26 | ```
27 |
28 | 4. Add `FAQ_SETTINGS = []` to your `settings.py`
29 | 5. Run ``python manage.py makemigrations`` to create the faq models migrations.
30 | 6. Run ``python manage.py migrate`` to create the faq models.
31 |
32 | 7. Start the development server and visit http://127.0.0.1:8000/admin/
33 | to create a category (you'll need the Admin app enabled).(categories part of the app can be disabled)
34 |
35 | 8. Visit http://127.0.0.1:8000/faq/ to see the categories.
36 |
37 | ## Settings
38 |
39 | you can change most things in settings below is a list of all settings
40 | add any or all to change to desired behavior::
41 |
42 |
43 | FAQ_SETTINGS = ['your_settings_here',]
44 |
45 |
46 | 1. no_category_description - add if using categories but don't want descriptions for them
47 | 2. no_category - add if don't want to use categories
48 | 3. logged_in_users_can_add_question - add if you want any logged in user to be able to ask a question
49 | 4. logged_in_users_can_answer_question - add if you want any logged in user to be able to answer a question
50 | 5. allow_multiple_answers - add if you want a question to be able to be answered multiple times
51 | 6. no_comments - add if don't want to use comments
52 | 7. anonymous_user_can_comment - add if you want any user to be able to comment including anonymous users
53 | 8. view_only_comments - add if you want users to see posted comments but not be able to add any more
54 | 9. no_votes - add if don't want any voting for useful questions or answers
55 | 10. no_answer_votes - add if only want question voting
56 | 11. no_question_votes - add if only want answer voting
57 | 12. allow_unicode - add if you want to allow unicode slugs
58 | 13. login_required - add if you want to only let logged in users see FAQ's
59 | 14. rich_text_answers - add if you want to use rich text for answers. This requires the django-tinymce package to be installed
60 |
61 | ## Templates
62 |
63 | all of the templates are meant to be overwritten
64 | to overwrite them create a faq directory inside of the templates directory and add a html file with the same name to it
65 |
66 | if this doesn't work make sure that the templates setting has 'DIRS': ['templates'], in it::
67 |
68 | TEMPLATES = [
69 | {
70 | ...
71 | 'DIRS': ['templates'],
72 | ...
73 | },
74 | ]
75 |
76 | here is a list of templates and there default template you can overwrite
77 |
78 | 1. categories_list.html - faq main view if using categories::
79 |
80 |
81 |
select a FAQ category
82 | {% for category in categories %}
83 |
84 | {% if category.description %}
85 | {{category.description}}
86 | {% endif %}
87 |
88 | {% endfor %}
89 |
90 |
91 | 2. category_detail.html - faq category detail view if using categories::
92 |
93 |
94 | choose a FAQ Question
95 | {{category}}
96 | {% if category.description %}
97 | {{category.description}}
98 | {% endif %}
99 |
100 | {% for question in category.question_set.all %}
101 |
102 | {% endfor %}
103 |
104 | back
105 | {% if can_add_question %}
106 | add question
107 | {% endif %}
108 |
109 |
110 | 3. questions_list.html - lists all questions if not using categories::
111 |
112 |
113 | choose a FAQ Question
114 | {% for question in questions %}
115 |
116 | {% endfor %}
117 |
118 | {% if can_add_question %}
119 |
120 | add question
121 | {% endif %}
122 |
123 |
124 | 4. question_detail.html - the question detail page::
125 |
126 |
127 | {% extends 'faq/question_base.html' %}
128 |
129 | {% block question_content %}
130 | {% if allow_multiple_answers %}
131 | answers
132 |
133 | {% for answer in question.answer_set.all %}
134 | {{answer.answer}}
135 | {% if can_vote_answer %}
136 | | found this answer helpful?
137 |
142 |
147 | {% endif %}
148 |
149 | {% endfor %}
150 |
151 |
152 | {% else %}
153 | {% if question.answer_set.exists %}
154 | answer:
155 | {{question.answer_set.first.answer}}
156 | {% if can_vote_answer %}
157 | found this answer helpful?
158 |
163 |
168 | {% endif %}
169 | {% else %}
170 | no answers yet
171 | {% endif %}
172 | {% endif %}
173 |
174 |
175 | {% if can_answer_question %}
176 | {% if category_enabled %}
177 | answer question
178 | {% else %}
179 | answer question
180 | {% endif %}
181 | {% endif %}
182 |
183 | {% if comments_allowed %}
184 | {% include 'faq/comments.html' %}
185 | {% endif %}
186 |
187 | {% endblock %}
188 |
189 | 5. answer_form.html - form to add answer to question::
190 |
191 |
192 | Answer Question
193 | {{question.question}}
194 |
199 |
200 | 6. comment_form.html - form to add comments to question (only shows up when form has error because view only gets posted to)::
201 |
202 |
203 | Post A Comment
204 | {{question.question}}
205 |
210 |
211 | 7. question_form.html - form to add a new question::
212 |
213 |
214 | Add Your Question
215 |
220 |
221 | 8. vote_form.html - form for voting questions and answers (only shows up when form has error because view only gets posted to)::
222 |
223 |
224 | vote
225 |
230 |
231 | 9. comments.html - if comments are allowed this template is included in the question detail.html::
232 |
233 |
234 | comments
235 |
236 | {% for comment in question.faqcomment_set.all %}
237 | {{comment.comment}}
238 | posted by {% if comment.user%}{{comment.user}}{% else %}anonymous{% endif %} {{comment.post_time|timesince}} ago
239 | {% endfor %}
240 |
241 | {% if add_new_comment_allowed %}
242 | {% if category_enabled %}
243 |
254 | {% endif %}
255 |
256 | ## Template Variables
257 |
258 | 1. categories_list.html
259 | categories - all the categories (category queryset)
260 |
261 | 2. categories_detail.html
262 | category - the category chosen (category object)
263 | can_add_question - bool if the user can add a question (depends on the settings)
264 | 3. questions_list.html
265 | questions - all the questions (question queryset)
266 | can_add_question - bool if the user can add a question (depends on the settings)
267 | 4. question_detail.html
268 | question - the question chosen (question object)
269 | can_vote_question - bool if the user can vote a question (depends on the settings)
270 | category_enabled - bool if category enabled in settings
271 | allow_multiple_answers - bool if multiple answers allowed in settings
272 | can_vote_answer - bool if the user can vote an answer (depends on the settings)
273 | can_answer_question - bool if current user can answer question (depends on the settings)
274 | comments_allowed - bool if using comments in settings
275 | add_new_comment_allowed - bool if current user can add comment (depends on the settings)
276 | comment_form - form to submit a new comment
277 | 5. answer_form.html
278 | question - the question to add answer to (question object)
279 | form - form to add new answer
280 | 6. comment_form.html
281 | question - the question to add comment to (question object)
282 | form - form to add new comment
283 | 7. question_form.html
284 | form - form to add new question
285 | 8. vote_form.html
286 | form - form to vote for a question or answer
287 |
288 | ## Urls
289 |
290 | all of the following urls are by name then additional
291 | the app name for the urls is ``'faq'``
292 |
293 | * index_view
294 | * no arguments
295 | * displays all the categories if categories are enabled otherwise shows questions
296 | * category_detail
297 | * needs category slug as slug
298 | * displays all the questions given the category when categories are enabled
299 | * add_question
300 | * if categories are enabled needs category slug as slug
301 | * if logged_in_users_can_add_question then displays form for logged in users to ask a new question
302 | * question_detail
303 | * needs question slug as question | if categories are enabled needs category slug as slug
304 | * displays the main FAQ page with the question all the comments and answers
305 | * answer_question
306 | * needs question slug as question | if categories are enabled needs category slug as category
307 | * displays the answer question form
308 | * add_comment
309 | * needs question slug as question | if categories are enabled needs category slug as category
310 | * only works if using comments
311 | * used to post comment form from question_detail to database
312 | * vote_answer
313 | * needs question slug as question | needs answer slug as answer | if categories are enabled needs category slug as category
314 | * only works if using answer voting
315 | * used to post hidden input vote = 1 or vote = 0 depending on vote up or down
316 | * vote_question
317 | * needs question slug as question | if categories are enabled needs category slug as category
318 | * only works if using question voting
319 | * used to post hidden input vote = 1 or vote = 0 depending on vote up or down
320 |
321 | ## django-tinymce
322 | If you want to use rich text answers you will need to [install django-tinymce](https://django-tinymce.readthedocs.io/en/latest/installation.html#id2)
323 |
324 | Make sure to include in the template the `{{ form.media }}` to include the tinymce javascript and css files.
325 | > [!WARNING]
326 | > Failing to follow the following steps will result in a xss vulnerability in your site.
327 |
328 | To allow the rich text answers to be rendered properly you will need to use the safe filter in your templates.
329 | While django-tinymce does escape the html the answers that were created when the rich text editor was not enabled **has not been escaped and is not safe**.
330 | So these answers cannot be rendered with the safe filter. So a flag was added to the answer model 'is_rich_text' that is set to True when the answer is created with the rich text editor.
331 | In the template you can use the following code to render the answer properly::
332 |
333 | {% if answer.is_rich_text %}
334 | {{answer.answer|safe}}
335 | {% else %}
336 | {{answer.answer}}
337 | {% endif %}
338 |
339 | ## Contributing
340 | django-easy-faq aims to be the best faq app for django. It welcomes contributions of all types - issues, bugs, feature requests, documentation updates, tests and pull requests.
341 |
342 | ## change log
343 | - 1.9 added view onsite link in admin, added richtext answers in admin
344 |
345 | - 1.8 added support for richtext answers with django-tinymce
346 |
347 | - 1.7 added support for django 5.0
348 |
349 | - 1.6 fixed bug where no_category_description did not do remove the category description in the admin
350 |
351 | - 1.5 added login_required setting to allow faq app to be available to only logged in users
352 |
353 | - 1.4 added unicode option to add unicode slugs
354 |
355 | - 1.3 fixed bug where a slug must be filled out in admin even though slug gets auto generated to save for questions, answers, and categories. Made questions, answers, categories slugs readonly in admin
356 |
357 | - 1.2 fixed bug in pypi distro not including faq app
358 |
359 | - 1.1 added more templates to override easily
360 |
361 | - 1.0 added pypi distribution
362 |
363 | - 0.5 fixed migrations
364 |
365 | - 0.4 fixed bug that logged out users can vote - which then raises exceptions
366 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/__init__.py
--------------------------------------------------------------------------------
/demo/default-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/default-1.png
--------------------------------------------------------------------------------
/demo/default-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/default-2.png
--------------------------------------------------------------------------------
/demo/default-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/default-3.png
--------------------------------------------------------------------------------
/demo/default-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/default-4.png
--------------------------------------------------------------------------------
/demo/demo-templates/answer_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Answer Question{% endblock %}
3 | {% block heading %}Answer Question{% endblock %}
4 | {% block breadcrumbs %}
5 |
6 |
7 | FAQ
8 | {% if question.category and category_enabled %}
9 | {{question.category}}
10 | {{question.question}}
11 | {% else %}
12 | {{question.question}}
13 | {% endif %}
14 | Answer Question
15 |
16 |
17 | {% endblock %}
18 | {% block content %}
19 | {{question.question}}
20 |
35 | {% endblock %}
36 |
--------------------------------------------------------------------------------
/demo/demo-templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
9 | {% block title %}FAQ with Bootstrap{% endblock %}
10 |
11 |
12 |
13 |
14 |
Navbar
15 |
16 |
17 |
18 |
46 |
47 |
48 | {% block breadcrumbs %}{% endblock %}
49 | {% block heading %}{% endblock %}
50 |
51 | {% block content %}
52 | {% endblock %}
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/demo/demo-templates/categories_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Choose Category{% endblock %}
3 | {% block heading %}Select a FAQ category{% endblock %}
4 |
5 | {% block content %}
6 |
7 | {% for category in categories %}
8 |
9 |
10 |
11 |
{{ category.name }}
12 | {% if category.description %}
13 |
{{category.description}}
14 | {% endif %}
15 |
View
16 |
17 |
18 |
19 | {% endfor %}
20 |
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------
/demo/demo-templates/category_detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Choose a FAQ Question{% endblock %}
3 | {% block heading %}{{category}}{% endblock %}
4 | {% block breadcrumbs %}
5 |
6 |
7 | FAQ
8 | {{category}}
9 |
10 |
11 | {% endblock %}
12 | {% block content %}
13 |
14 | {% if category.description %}
15 | {{category.description}}
16 | {% endif %}
17 |
18 | {% for question in category.question_set.all %}
19 |
20 | {% endfor %}
21 |
22 | Back
23 | {% if can_add_question %}
24 | Add Question
25 | {% endif %}
26 | {% endblock %}
27 |
--------------------------------------------------------------------------------
/demo/demo-templates/comment_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/question_base.html' %}
2 |
3 | {% block question_content %}
4 | Post A Comment
5 | {{question.question}}
6 |
11 | {% endblock %}
--------------------------------------------------------------------------------
/demo/demo-templates/comments.html:
--------------------------------------------------------------------------------
1 |
2 |
Comments
3 | {% for comment in question.faqcomment_set.all %}
4 |
5 |
{{comment.comment}}
6 | posted by {% if comment.user%}{{comment.user}}{% else %}anonymous{% endif %} {{comment.post_time|timesince}} ago
7 |
8 | {% endfor %}
9 |
10 |
11 | {% if add_new_comment_allowed %}
12 |
13 |
14 | {% if category_enabled %}
15 |
34 | {% endif %}
35 |
--------------------------------------------------------------------------------
/demo/demo-templates/question_base.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | {{question.question|title}}{% endblock %}
3 | {% block heading %}{{question.question|title}} {% if can_vote_question %}
4 |
9 |
14 | {% endif %}{% endblock %}
15 | {% block breadcrumbs %}
16 |
17 |
18 | FAQ
19 | {% if question.category and category_enabled %}
20 | {{question.category}}
21 | {% endif %}
22 | {{ question.question }}
23 |
24 |
25 | {% endblock %}
26 | {% block content %}
27 |
28 | {% if question.category and category_enabled %}
29 | {{question.category.name}}
30 | {% endif %}
31 |
32 |
33 |
34 | {% block question_content %}
35 | {% endblock %}
36 |
37 | {% endblock %}
--------------------------------------------------------------------------------
/demo/demo-templates/question_detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/question_base.html' %}
2 |
3 | {% block question_content %}
4 | {% if allow_multiple_answers %}
5 | Answers
6 |
7 | {% for answer in question.answer_set.all %}
8 |
9 |
10 |
11 |
{{answer.answer}}
12 |
13 | {% if can_vote_answer %}
14 |
15 | found this helpful?
16 |
21 |
26 |
27 | {% endif %}
28 |
29 |
30 | {% endfor %}
31 |
32 |
33 | {% else %}
34 | {% if question.answer_set.exists %}
35 | Answer:
36 | {{question.answer_set.first.answer}}
37 | {% if can_vote_answer %}
38 | found this answer helpful?
39 |
44 |
49 | {% endif %}
50 | {% else %}
51 | no answers yet
52 | {% endif %}
53 | {% endif %}
54 |
55 |
56 | {% if can_answer_question %}
57 | {% if category_enabled %}
58 | answer question
59 | {% else %}
60 | answer question
61 | {% endif %}
62 | {% endif %}
63 |
64 | {% if comments_allowed %}
65 | {% include 'faq/comments.html' %}
66 | {% endif %}
67 |
68 | {% endblock %}
--------------------------------------------------------------------------------
/demo/demo-templates/question_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Ask a Question{% endblock %}
3 | {% block heading %}Ask a Question{% endblock %}
4 | {% block breadcrumbs %}
5 |
6 |
7 | FAQ
8 | {% if category %}
9 | {{category}}
10 | {% endif %}
11 | Ask Question
12 |
13 |
14 | {% endblock %}
15 | {% block content %}
16 |
31 | {% endblock %}
32 |
--------------------------------------------------------------------------------
/demo/demo-templates/questions_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Choose Question{% endblock %}
3 | {% block heading %}Choose a FAQ Question{% endblock %}
4 | {% block content %}
5 | {% for question in questions %}
6 |
7 | {% endfor %}
8 |
9 | {% if can_add_question %}
10 |
11 | add question
12 | {% endif %}
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/demo/demo-templates/vote_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Vote{% endblock %}
3 | {% block heading %}Vote{% endblock %}
4 | {% block content %}
5 |
10 | {% endblock %}
--------------------------------------------------------------------------------
/demo/demo.md:
--------------------------------------------------------------------------------
1 | # Demo
2 | This is sample of what django-easy-faq can look like with [Bootstrap 5](https://getbootstrap.com/) styled templates.
3 | This is here to demonstrate what this django app provides and what features it supports.
4 | Below is some screenshots with the default settings and of what it looks like with some of the other settings turned on.
5 |
6 | ### with no settings
7 |
8 | This is for a user that is logged in when the faq app has no settings set.
9 |
10 | /faq/
11 | 
12 | /faq/category-1/
13 | 
14 | /faq/category-1/do-you-offer-any-discounts-or-promotions/
15 | 
16 | with an answer
17 | 
18 | ### no_category_description
19 |
20 | Categories don't have category descriptions.
21 |
22 | /faq/
23 | 
24 |
25 | /faq/category-1/
26 | 
27 | ### no_category
28 |
29 | there are no categories for questions. All questions show on the faq index page
30 |
31 | /faq/
32 | 
33 |
34 | /faq/do-you-offer-any-discounts-or-promotions/
35 | 
36 |
37 | ### logged_in_users_can_add_question
38 |
39 | allow logged in users to ask questions
40 |
41 | /faq/category-1/
42 | 
43 |
44 | /faq/category-1/add/question/
45 | 
46 |
47 | ### logged_in_users_can_answer_question
48 |
49 | allow users that are logged in to be able to answer the questions
50 |
51 | /faq/category-1/what-payment-methods-do-you-accept/
52 | 
53 |
54 | /faq/category-1/what-payment-methods-do-you-accept/answer/
55 | 
56 |
57 | ### allow_multiple_answers
58 |
59 | More than one answer can exist for the question. If `logged_in_users_can_answer_question` is also set then users can add their own answers to the question.
60 |
61 | /faq/category-1/what-is-your-return-policy/
62 | 
63 |
64 | ### no_comments
65 |
66 | showing or adding comments to questions is removed
67 |
68 | /faq/category-1/do-you-offer-any-discounts-or-promotions/
69 | 
70 |
71 | ### anonymous_user_can_comment
72 |
73 | even users that are not logged in can comment
74 |
75 | /faq/category-1/do-you-offer-any-discounts-or-promotions/
76 | 
77 |
78 | ### view_only_comments
79 |
80 | users can only view the comments
81 |
82 | /faq/category-1/do-you-offer-any-discounts-or-promotions/
83 | 
84 |
85 | ### no_votes
86 |
87 | users cannot vote on questions or answers
88 |
89 | /faq/category-1/do-you-offer-any-discounts-or-promotions/
90 | 
91 |
92 | ### no_answer_votes
93 |
94 | can only vote on questions
95 |
96 | /faq/category-1/do-you-offer-any-discounts-or-promotions/
97 | 
98 |
99 | ### no_question_votes
100 |
101 | can only vote on answers
102 |
103 | /faq/category-1/do-you-offer-any-discounts-or-promotions/
104 | 
105 |
--------------------------------------------------------------------------------
/demo/option-1-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/option-1-1.png
--------------------------------------------------------------------------------
/demo/option-1-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/option-1-2.png
--------------------------------------------------------------------------------
/demo/option-10-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/option-10-1.png
--------------------------------------------------------------------------------
/demo/option-11-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/option-11-1.png
--------------------------------------------------------------------------------
/demo/option-2-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/option-2-1.png
--------------------------------------------------------------------------------
/demo/option-2-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/option-2-2.png
--------------------------------------------------------------------------------
/demo/option-3-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/option-3-1.png
--------------------------------------------------------------------------------
/demo/option-3-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/option-3-2.png
--------------------------------------------------------------------------------
/demo/option-4-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/option-4-1.png
--------------------------------------------------------------------------------
/demo/option-4-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/option-4-2.png
--------------------------------------------------------------------------------
/demo/option-5-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/option-5-1.png
--------------------------------------------------------------------------------
/demo/option-6-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/option-6-1.png
--------------------------------------------------------------------------------
/demo/option-8-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/option-8-1.png
--------------------------------------------------------------------------------
/demo/option-9-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/demo/option-9-1.png
--------------------------------------------------------------------------------
/example/example/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/example/example/__init__.py
--------------------------------------------------------------------------------
/example/example/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for example project.
3 |
4 | Generated by 'django-admin startproject' using Django 5.1.dev20240220195926.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/dev/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/dev/ref/settings/
11 | """
12 |
13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
14 | import os
15 |
16 | PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17 | BASE_DIR = os.path.dirname(PROJECT_DIR)
18 |
19 | DEBUG = True
20 |
21 | SECRET_KEY = "django-insecure-tkvp+i6^@nq@v2c19720n_hhd$p_)v8ei)bw2%kte!@ihci7))"
22 |
23 | ALLOWED_HOSTS = ["*"]
24 | # Quick-start development settings - unsuitable for production
25 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/
26 |
27 |
28 | # Application definition
29 |
30 | INSTALLED_APPS = [
31 | "home",
32 | "django.contrib.admin",
33 | "django.contrib.auth",
34 | "django.contrib.contenttypes",
35 | "django.contrib.sessions",
36 | "django.contrib.messages",
37 | "django.contrib.staticfiles",
38 | 'faq',
39 | 'tinymce',
40 | ]
41 |
42 | MIDDLEWARE = [
43 | "django.contrib.sessions.middleware.SessionMiddleware",
44 | "django.middleware.common.CommonMiddleware",
45 | "django.middleware.csrf.CsrfViewMiddleware",
46 | "django.contrib.auth.middleware.AuthenticationMiddleware",
47 | "django.contrib.messages.middleware.MessageMiddleware",
48 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
49 | "django.middleware.security.SecurityMiddleware",
50 | ]
51 |
52 | ROOT_URLCONF = "example.urls"
53 |
54 | TEMPLATES = [
55 | {
56 | "BACKEND": "django.template.backends.django.DjangoTemplates",
57 | "DIRS": [
58 | os.path.join(PROJECT_DIR, "templates"),
59 | ],
60 | "APP_DIRS": True,
61 | "OPTIONS": {
62 | "context_processors": [
63 | "django.template.context_processors.debug",
64 | "django.template.context_processors.request",
65 | "django.contrib.auth.context_processors.auth",
66 | "django.contrib.messages.context_processors.messages",
67 | ],
68 | },
69 | },
70 | ]
71 |
72 | WSGI_APPLICATION = "example.wsgi.application"
73 |
74 |
75 | # Database
76 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases
77 |
78 | DATABASES = {
79 | "default": {
80 | "ENGINE": "django.db.backends.sqlite3",
81 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
82 | }
83 | }
84 |
85 |
86 | # Password validation
87 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
88 |
89 | AUTH_PASSWORD_VALIDATORS = [
90 | {
91 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
92 | },
93 | {
94 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
95 | },
96 | {
97 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
98 | },
99 | {
100 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
101 | },
102 | ]
103 |
104 |
105 | # Internationalization
106 | # https://docs.djangoproject.com/en/dev/topics/i18n/
107 |
108 | LANGUAGE_CODE = "en-us"
109 |
110 | TIME_ZONE = "UTC"
111 |
112 | USE_I18N = True
113 |
114 | USE_TZ = True
115 |
116 |
117 | # Static files (CSS, JavaScript, Images)
118 | # https://docs.djangoproject.com/en/dev/howto/static-files/
119 |
120 | STATICFILES_FINDERS = [
121 | "django.contrib.staticfiles.finders.FileSystemFinder",
122 | "django.contrib.staticfiles.finders.AppDirectoriesFinder",
123 | ]
124 |
125 | STATICFILES_DIRS = [
126 | os.path.join(PROJECT_DIR, "static"),
127 | ]
128 |
129 | STATIC_ROOT = os.path.join(BASE_DIR, "static")
130 | STATIC_URL = "/static/"
131 |
132 | MEDIA_ROOT = os.path.join(BASE_DIR, "media")
133 | MEDIA_URL = "/media/"
134 |
135 | # Default storage settings, with the staticfiles storage updated.
136 | # See https://docs.djangoproject.com/en/dev/ref/settings/#std-setting-STORAGES
137 | STORAGES = {
138 | "default": {
139 | "BACKEND": "django.core.files.storage.FileSystemStorage",
140 | },
141 | # ManifestStaticFilesStorage is recommended in production, to prevent
142 | # outdated JavaScript / CSS assets being served from cache
143 | # (e.g. after a Wagtail upgrade).
144 | # See https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#manifeststaticfilesstorage
145 | "staticfiles": {
146 | "BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
147 | },
148 | }
149 |
150 | EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
151 |
152 | # https://github.com/smark-1/django-easy-faq
153 | # Django easy FAQ settings
154 | FAQ_SETTINGS = [
155 | 'logged_in_users_can_answer_question',
156 | 'allow_multiple_answers',
157 | 'rich_text_answers',
158 | 'logged_in_users_can_add_question',
159 | ]
--------------------------------------------------------------------------------
/example/example/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path, include
2 | from django.contrib import admin
3 | from home.views import home
4 |
5 | urlpatterns = [
6 | path("", home),
7 | path('faq/', include('faq.urls')),
8 | path("admin/", admin.site.urls),
9 | path('tinymce/', include('tinymce.urls')),
10 | ]
11 |
12 |
--------------------------------------------------------------------------------
/example/example/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for example project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/dev/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings.dev")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/example/home/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/example/home/__init__.py
--------------------------------------------------------------------------------
/example/home/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/example/home/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class HomeConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'home'
7 |
--------------------------------------------------------------------------------
/example/home/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/example/home/migrations/__init__.py
--------------------------------------------------------------------------------
/example/home/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/example/home/templates/home/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 |
3 | {% block title %}Example app{% endblock %}
4 | {% block heading %}Example FAQ{% endblock %}
5 | {% block content %}
6 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/example/home/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/example/home/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 |
3 |
4 | # Create your views here.
5 | def home(request):
6 | return render(request, 'home/index.html')
7 |
--------------------------------------------------------------------------------
/example/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/example/requirements.txt:
--------------------------------------------------------------------------------
1 | Django>=4.2,<5.2
2 | django-easy-faq<=1.9
--------------------------------------------------------------------------------
/example/templates/faq/answer_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Answer Question{% endblock %}
3 | {% block heading %}Answer Question{% endblock %}
4 | {% block breadcrumbs %}
5 |
6 |
7 | FAQ
8 | {% if question.category and category_enabled %}
9 | {{question.category}}
10 | {{question.question}}
11 | {% else %}
12 | {{question.question}}
13 | {% endif %}
14 | Answer Question
15 |
16 |
17 | {% endblock %}
18 | {% block content %}
19 | {{ form.media }}
20 | {{question.question}}
21 |
37 | {% endblock %}
38 |
--------------------------------------------------------------------------------
/example/templates/faq/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
9 | {% block title %}FAQ with Bootstrap{% endblock %}
10 |
11 |
12 |
13 |
14 |
Navbar
15 |
16 |
17 |
18 |
46 |
47 |
48 | {% block breadcrumbs %}{% endblock %}
49 | {% block heading %}{% endblock %}
50 |
51 | {% block content %}
52 | {% endblock %}
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/example/templates/faq/categories_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Choose Category{% endblock %}
3 | {% block heading %}Select a FAQ category{% endblock %}
4 |
5 | {% block content %}
6 |
7 | {% for category in categories %}
8 |
9 |
10 |
11 |
{{ category.name }}
12 | {% if category.description %}
13 |
{{category.description}}
14 | {% endif %}
15 |
View
16 |
17 |
18 |
19 | {% endfor %}
20 |
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------
/example/templates/faq/category_detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Choose a FAQ Question{% endblock %}
3 | {% block heading %}{{category}}{% endblock %}
4 | {% block breadcrumbs %}
5 |
6 |
7 | FAQ
8 | {{category}}
9 |
10 |
11 | {% endblock %}
12 | {% block content %}
13 |
14 | {% if category.description %}
15 | {{category.description}}
16 | {% endif %}
17 |
18 | {% for question in category.question_set.all %}
19 |
20 | {% endfor %}
21 |
22 | Back
23 | {% if can_add_question %}
24 | Add Question
25 | {% endif %}
26 | {% endblock %}
27 |
--------------------------------------------------------------------------------
/example/templates/faq/comment_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/question_base.html' %}
2 |
3 | {% block question_content %}
4 | Post A Comment
5 | {{question.question}}
6 |
11 | {% endblock %}
--------------------------------------------------------------------------------
/example/templates/faq/comments.html:
--------------------------------------------------------------------------------
1 |
2 |
Comments
3 | {% for comment in question.faqcomment_set.all %}
4 |
5 |
{{comment.comment}}
6 | posted by {% if comment.user%}{{comment.user}}{% else %}anonymous{% endif %} {{comment.post_time|timesince}} ago
7 |
8 | {% endfor %}
9 |
10 |
11 | {% if add_new_comment_allowed %}
12 |
13 |
14 | {% if category_enabled %}
15 |
34 | {% endif %}
35 |
--------------------------------------------------------------------------------
/example/templates/faq/question_base.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | {{question.question|title}}{% endblock %}
3 | {% block heading %}{{question.question|title}} {% if can_vote_question %}
4 |
9 |
14 | {% endif %}{% endblock %}
15 | {% block breadcrumbs %}
16 |
17 |
18 | FAQ
19 | {% if question.category and category_enabled %}
20 | {{question.category}}
21 | {% endif %}
22 | {{ question.question }}
23 |
24 |
25 | {% endblock %}
26 | {% block content %}
27 |
28 | {% if question.category and category_enabled %}
29 | {{question.category.name}}
30 | {% endif %}
31 |
32 |
33 |
34 | {% block question_content %}
35 | {% endblock %}
36 |
37 | {% endblock %}
--------------------------------------------------------------------------------
/example/templates/faq/question_detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/question_base.html' %}
2 |
3 | {% block question_content %}
4 | {% if allow_multiple_answers %}
5 | Answers
6 |
7 | {% for answer in question.answer_set.all %}
8 |
9 |
10 |
11 | {% if answer.is_rich_text %}
12 |
{{ answer.answer|safe }}
13 | {% else %}
14 |
{{answer.answer}}
15 | {% endif %}
16 |
17 | {% if can_vote_answer %}
18 |
19 | found this helpful?
20 |
25 |
30 |
31 | {% endif %}
32 |
33 |
34 | {% endfor %}
35 |
36 |
37 | {% else %}
38 | {% if question.answer_set.exists %}
39 | Answer:
40 | {% if question.answer_set.first.is_rich_text %}
41 | {{ question.answer_set.first.answer|safe }}
42 | {% else %}
43 | {{question.answer_set.first.answer}}
44 | {% endif %}
45 |
46 | {% if can_vote_answer %}
47 | found this answer helpful?
48 |
53 |
58 | {% endif %}
59 | {% else %}
60 | no answers yet
61 | {% endif %}
62 | {% endif %}
63 |
64 |
65 | {% if can_answer_question %}
66 | {% if category_enabled %}
67 | answer question
68 | {% else %}
69 | answer question
70 | {% endif %}
71 | {% endif %}
72 |
73 | {% if comments_allowed %}
74 | {% include 'faq/comments.html' %}
75 | {% endif %}
76 |
77 | {% endblock %}
--------------------------------------------------------------------------------
/example/templates/faq/question_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Ask a Question{% endblock %}
3 | {% block heading %}Ask a Question{% endblock %}
4 | {% block breadcrumbs %}
5 |
6 |
7 | FAQ
8 | {% if category %}
9 | {{category}}
10 | {% endif %}
11 | Ask Question
12 |
13 |
14 | {% endblock %}
15 | {% block content %}
16 |
31 | {% endblock %}
32 |
--------------------------------------------------------------------------------
/example/templates/faq/questions_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Choose Question{% endblock %}
3 | {% block heading %}Choose a FAQ Question{% endblock %}
4 | {% block content %}
5 | {% for question in questions %}
6 |
7 | {% endfor %}
8 |
9 | {% if can_add_question %}
10 |
11 | add question
12 | {% endif %}
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/example/templates/faq/vote_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Vote{% endblock %}
3 | {% block heading %}Vote{% endblock %}
4 | {% block content %}
5 |
10 | {% endblock %}
--------------------------------------------------------------------------------
/faq/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'faq.apps.FaqConfig'
--------------------------------------------------------------------------------
/faq/admin.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.contrib import admin
3 | from .models import *
4 | from django.conf import settings
5 | from django.utils.html import strip_tags
6 | # Register your models here.
7 |
8 | class AnswerHelpfulAdmin(admin.ModelAdmin):
9 | list_display = ("vote", "answer", "user")
10 | list_filter = ('vote',)
11 | search_fields = ['answer', "user"]
12 |
13 |
14 | class QuestionHelpfulAdmin(admin.ModelAdmin):
15 | list_display = ("vote", "question", "user")
16 | list_filter = ('vote',)
17 | search_fields = ['question', "user"]
18 |
19 |
20 | class AnswerAdminForm(forms.ModelForm):
21 | class Meta:
22 | model = Answer
23 | fields = "__all__"
24 |
25 | def __init__(self, *args, **kwargs):
26 | super().__init__(*args, **kwargs)
27 | if self.instance.pk:
28 | if self.instance.is_rich_text:
29 | try:
30 | from tinymce.widgets import TinyMCE
31 | self.fields["answer"].widget = TinyMCE()
32 | except ImportError:
33 | raise ImportError("Please install django-tinymce to use rich text answers")
34 | elif "rich_text_answers" in settings.FAQ_SETTINGS:
35 | self.instance.is_rich_text = True
36 | try:
37 | from tinymce.widgets import TinyMCE
38 | self.fields["answer"].widget = TinyMCE()
39 | except ImportError:
40 | raise ImportError("Please install django-tinymce to use rich text answers")
41 |
42 |
43 | class AnswerAdmin(admin.ModelAdmin):
44 | list_display = ("answer_", "question", "helpful", "not_helpful",'is_rich_text')
45 | list_filter = ('helpful', "not_helpful",'is_rich_text')
46 | search_fields = ['answer', "question"]
47 | readonly_fields = ('helpful', "not_helpful", 'slug','is_rich_text')
48 | form = AnswerAdminForm
49 | def answer_(self, obj):
50 | if obj.is_rich_text:
51 | return strip_tags(obj.answer)
52 | return obj.answer
53 | class CategoryAdmin(admin.ModelAdmin):
54 | search_fields = ['name', "_description"]
55 | readonly_fields = ("slug",)
56 |
57 | def get_list_display(self, request):
58 | if "no_category_description" not in settings.FAQ_SETTINGS:
59 | return ["name", "slug", "description"]
60 | return ['name', 'slug']
61 |
62 | def get_exclude(self, request, obj=None):
63 | if "no_category_description" in settings.FAQ_SETTINGS:
64 | return ['_description']
65 | else:
66 | return None
67 |
68 | class CommentAdmin(admin.ModelAdmin):
69 | list_display = ("comment", "question", "user", "post_time")
70 | list_filter = ('question', "post_time")
71 | search_fields = ['comment', "question"]
72 |
73 |
74 | class QuestionAdmin(admin.ModelAdmin):
75 | list_display = ("question", "category", "slug", "helpful", "not_helpful")
76 | list_filter = ('helpful', "not_helpful", "category")
77 | search_fields = ["question"]
78 | readonly_fields = ('helpful', "not_helpful", "slug")
79 |
80 |
81 | admin.site.register(Question, QuestionAdmin)
82 | admin.site.register(Answer, AnswerAdmin)
83 |
84 | # if category enabled
85 | if "no_category" not in settings.FAQ_SETTINGS:
86 | admin.site.register(Category, CategoryAdmin)
87 |
88 | # if comments are enabled
89 | if "no_comments" not in settings.FAQ_SETTINGS:
90 | admin.site.register(FAQComment, CommentAdmin)
91 |
92 | # if votes are enabled
93 | if "no_votes" not in settings.FAQ_SETTINGS:
94 | # if answer votes are enabled
95 | if "no_answer_votes" not in settings.FAQ_SETTINGS:
96 | admin.site.register(AnswerHelpful, AnswerHelpfulAdmin)
97 |
98 | # if question votes are enabled
99 | if "no_question_votes" not in settings.FAQ_SETTINGS:
100 | admin.site.register(QuestionHelpful, QuestionHelpfulAdmin)
101 |
--------------------------------------------------------------------------------
/faq/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class FaqConfig(AppConfig):
5 | name = 'faq'
6 | verbose_name = "faq"
7 | default_auto_field = 'django.db.models.BigAutoField'
8 |
--------------------------------------------------------------------------------
/faq/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.conf import settings
3 | from . import models
4 |
5 | class AnswerForm(forms.ModelForm):
6 | class Meta:
7 | model = models.Answer
8 | fields = ["answer"]
9 |
10 | def __init__(self, *args, **kwargs):
11 | super().__init__(*args, **kwargs)
12 | if "rich_text_answers" in settings.FAQ_SETTINGS:
13 | try:
14 | from tinymce.widgets import TinyMCE
15 | self.fields["answer"].widget = TinyMCE()
16 | except ImportError:
17 | raise ImportError("Please install django-tinymce to use rich text answers")
18 |
19 | class CommentForm(forms.ModelForm):
20 | class Meta:
21 | model = models.FAQComment
22 | fields = ["comment"]
23 |
24 |
25 | class VoteForm(forms.Form):
26 | vote = forms.BooleanField(label="helpful? ", required=False)
27 |
--------------------------------------------------------------------------------
/faq/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.7 on 2021-02-28 16:07
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Answer',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('answer', models.TextField()),
22 | ('slug', models.SlugField(max_length=10)),
23 | ('helpful', models.IntegerField(default=0)),
24 | ('not_helpful', models.IntegerField(default=0)),
25 | ],
26 | ),
27 | migrations.CreateModel(
28 | name='Category',
29 | fields=[
30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
31 | ('name', models.CharField(max_length=50, unique=True)),
32 | ('description', models.TextField()),
33 | ('slug', models.SlugField(unique=True)),
34 | ],
35 | options={
36 | 'verbose_name_plural': 'categories',
37 | },
38 | ),
39 | migrations.CreateModel(
40 | name='Question',
41 | fields=[
42 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
43 | ('question', models.CharField(max_length=150, unique=True)),
44 | ('slug', models.SlugField(max_length=150, unique=True)),
45 | ('helpful', models.IntegerField(default=0)),
46 | ('not_helpful', models.IntegerField(default=0)),
47 | ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='faq.category')),
48 | ],
49 | ),
50 | migrations.CreateModel(
51 | name='QuestionHelpful',
52 | fields=[
53 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
54 | ('vote', models.BooleanField()),
55 | ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='faq.question')),
56 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
57 | ],
58 | options={
59 | 'ordering': ['question', 'vote'],
60 | },
61 | ),
62 | migrations.CreateModel(
63 | name='FAQComment',
64 | fields=[
65 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
66 | ('comment', models.TextField()),
67 | ('post_time', models.DateTimeField(auto_now_add=True)),
68 | ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='faq.question')),
69 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
70 | ],
71 | options={
72 | 'ordering': ['question', '-post_time'],
73 | },
74 | ),
75 | migrations.CreateModel(
76 | name='AnswerHelpful',
77 | fields=[
78 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
79 | ('vote', models.BooleanField()),
80 | ('answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='faq.answer')),
81 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
82 | ],
83 | options={
84 | 'ordering': ['answer', 'vote'],
85 | },
86 | ),
87 | migrations.AddField(
88 | model_name='answer',
89 | name='question',
90 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='faq.question'),
91 | ),
92 | migrations.AlterOrderWithRespectTo(
93 | name='answer',
94 | order_with_respect_to='question',
95 | ),
96 | ]
97 |
--------------------------------------------------------------------------------
/faq/migrations/0002_alter_answer_id_alter_answerhelpful_id_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.0 on 2021-12-19 14:31
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('faq', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='answer',
15 | name='id',
16 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
17 | ),
18 | migrations.AlterField(
19 | model_name='answerhelpful',
20 | name='id',
21 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
22 | ),
23 | migrations.AlterField(
24 | model_name='category',
25 | name='id',
26 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
27 | ),
28 | migrations.AlterField(
29 | model_name='faqcomment',
30 | name='id',
31 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
32 | ),
33 | migrations.AlterField(
34 | model_name='question',
35 | name='id',
36 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
37 | ),
38 | migrations.AlterField(
39 | model_name='questionhelpful',
40 | name='id',
41 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
42 | ),
43 | ]
44 |
--------------------------------------------------------------------------------
/faq/migrations/0003_auto_20220619_0939.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2022-06-19 09:39
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('faq', '0002_alter_answer_id_alter_answerhelpful_id_and_more'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='question',
15 | name='slug',
16 | field=models.SlugField(blank=True, max_length=150, unique=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/faq/migrations/0004_alter_answer_slug_alter_category_slug.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.0 on 2022-06-19 16:09
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('faq', '0003_auto_20220619_0939'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='answer',
15 | name='slug',
16 | field=models.SlugField(blank=True, max_length=10),
17 | ),
18 | migrations.AlterField(
19 | model_name='category',
20 | name='slug',
21 | field=models.SlugField(blank=True, unique=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/faq/migrations/0005_rename_description_category__description.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.6 on 2023-05-05 20:01
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('faq', '0004_alter_answer_slug_alter_category_slug'),
10 | ]
11 |
12 | operations = [
13 | migrations.RenameField(
14 | model_name='category',
15 | old_name='description',
16 | new_name='_description',
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/faq/migrations/0006_answer_is_rich_text.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.0.4 on 2024-05-08 01:14
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('faq', '0005_rename_description_category__description'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='answer',
15 | name='is_rich_text',
16 | field=models.BooleanField(default=False),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/faq/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smark-1/django-easy-faq/fa0c7c03ff815d8c0b3b1970e4bdb3f8563250b0/faq/migrations/__init__.py
--------------------------------------------------------------------------------
/faq/models.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.db import models
3 | from django.utils.text import slugify
4 | from django.shortcuts import reverse
5 |
6 | from . import snippets
7 |
8 |
9 | # Create your models here.
10 | class Question(models.Model):
11 | category = models.ForeignKey("category", on_delete=models.SET_NULL, null=True, blank=True)
12 | question = models.CharField(max_length=150, unique=True)
13 | slug = models.SlugField(max_length=150, unique=True, blank=True)
14 | helpful = models.IntegerField(default=0)
15 | not_helpful = models.IntegerField(default=0)
16 |
17 | def get_helpful(self):
18 | return QuestionHelpful.objects.filter(question=self, vote=True).count()
19 |
20 | def get_not_helpful(self):
21 | return QuestionHelpful.objects.filter(question=self, vote=False).count()
22 |
23 | def __str__(self):
24 | return self.question
25 |
26 | def save(self, *args, **kwargs):
27 | self.slug = slugify(self.question, allow_unicode='allow_unicode' in settings.FAQ_SETTINGS)[:150]
28 | # if question already exists
29 | if self.pk:
30 | self.helpful = self.get_helpful()
31 | self.not_helpful = self.get_not_helpful()
32 | return super().save(*args, **kwargs)
33 |
34 | def get_absolute_url(self):
35 | # if using categories
36 | if "no_category" not in settings.FAQ_SETTINGS:
37 | return reverse("faq:question_detail", args=(self.category.slug, self.slug))
38 | else:
39 | return reverse("faq:question_detail", args=(self.slug,))
40 |
41 | class Meta:
42 | app_label = 'faq'
43 |
44 |
45 | class Answer(models.Model):
46 | question = models.ForeignKey(Question, on_delete=models.CASCADE)
47 | answer = models.TextField()
48 | slug = models.SlugField(max_length=10, blank=True)
49 | helpful = models.IntegerField(default=0)
50 | not_helpful = models.IntegerField(default=0)
51 | is_rich_text = models.BooleanField(default=False)
52 |
53 | def get_helpful(self):
54 | return AnswerHelpful.objects.filter(answer=self, vote=True).count()
55 |
56 | def get_not_helpful(self):
57 | return AnswerHelpful.objects.filter(answer=self, vote=False).count()
58 |
59 | def __str__(self):
60 | return self.answer
61 |
62 | class Meta:
63 | order_with_respect_to = 'question'
64 | app_label = 'faq'
65 |
66 | def save(self, *args, **kwargs):
67 | # if first time saving add a new slug
68 | if not self.pk or not self.slug:
69 | new_slug = snippets.create_random_slug(5)
70 | while Answer.objects.filter(slug=new_slug, answer=self.answer).exists():
71 | new_slug = snippets.create_random_slug()
72 | self.slug = new_slug
73 | # if answer is not new
74 | if self.pk:
75 | self.helpful = self.get_helpful()
76 | self.not_helpful = self.get_not_helpful()
77 | super().save(*args, **kwargs)
78 |
79 | def get_absolute_url(self):
80 | # if using categories
81 | if "no_category" not in settings.FAQ_SETTINGS:
82 | return reverse("faq:question_detail", args=(self.question.category.slug, self.question.slug))
83 | else:
84 | return reverse("faq:question_detail", args=(self.question.slug,))
85 |
86 |
87 | class Category(models.Model):
88 | name = models.CharField(max_length=50, unique=True)
89 | _description = models.TextField()
90 | slug = models.SlugField(max_length=50, unique=True, blank=True)
91 |
92 | def __str__(self):
93 | return self.name
94 |
95 | class Meta:
96 | verbose_name_plural = "categories"
97 | app_label = 'faq'
98 |
99 | @property
100 | def description(self):
101 | """only show the description is categories have descriptions"""
102 | if "no_category_description" in settings.FAQ_SETTINGS:
103 | return None
104 | else:
105 | return self._description
106 |
107 | def save(self, *args, **kwargs):
108 | self.slug = slugify(self.name, allow_unicode='allow_unicode' in settings.FAQ_SETTINGS)[:50]
109 | return super().save(*args, **kwargs)
110 |
111 | def get_absolute_url(self):
112 | return reverse("faq:category_detail", args=(self.slug,))
113 |
114 |
115 | class FAQComment(models.Model):
116 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
117 | comment = models.TextField()
118 | question = models.ForeignKey(Question, on_delete=models.CASCADE)
119 | post_time = models.DateTimeField(auto_now_add=True)
120 |
121 | def __str__(self):
122 | return self.comment
123 |
124 | class Meta:
125 | ordering = ['question', '-post_time']
126 | app_label = 'faq'
127 |
128 | def get_absolute_url(self):
129 | # if using categories
130 | if "no_category" not in settings.FAQ_SETTINGS:
131 | return reverse("faq:question_detail", args=(self.question.category.slug, self.question.slug))
132 | else:
133 | return reverse("faq:question_detail", args=(self.question.slug,))
134 |
135 |
136 | class AnswerHelpful(models.Model):
137 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
138 | answer = models.ForeignKey(Answer, on_delete=models.CASCADE)
139 | vote = models.BooleanField()
140 |
141 | def __str__(self):
142 | if self.vote:
143 | vote_var = 'like'
144 | else:
145 | vote_var = 'dislike'
146 |
147 | return str(self.answer) + '- ' + vote_var
148 |
149 | class Meta:
150 | ordering = ['answer', 'vote']
151 | app_label = 'faq'
152 |
153 | def get_absolute_url(self):
154 | # if using categories
155 | if "no_category" not in settings.FAQ_SETTINGS:
156 | return reverse("faq:question_detail", args=(self.answer.question.category.slug, self.answer.question.slug))
157 | else:
158 | return reverse("faq:question_detail", args=(self.answer.question.slug,))
159 |
160 |
161 |
162 | class QuestionHelpful(models.Model):
163 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
164 | question = models.ForeignKey(Question, on_delete=models.CASCADE)
165 | vote = models.BooleanField()
166 |
167 | def __str__(self):
168 | if self.vote:
169 | vote_var = 'like'
170 | else:
171 | vote_var = 'dislike'
172 |
173 | return str(self.question) + '- ' + vote_var
174 |
175 | class Meta:
176 | ordering = ['question', 'vote']
177 | app_label = 'faq'
178 |
179 | def get_absolute_url(self):
180 | # if using categories
181 | if "no_category" not in settings.FAQ_SETTINGS:
182 | return reverse("faq:question_detail", args=(self.question.category.slug, self.question.slug))
183 | else:
184 | return reverse("faq:question_detail", args=(self.question.slug,))
185 |
--------------------------------------------------------------------------------
/faq/path_converters.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.urls.converters import SlugConverter, register_converter
3 |
4 | if 'allow_unicode' in settings.FAQ_SETTINGS:
5 | class UnicodeSlugConverter(SlugConverter):
6 | regex = '[0-9\w_-]+'
7 |
8 |
9 | register_converter(UnicodeSlugConverter, 'uslug')
10 |
11 | else:
12 | register_converter(SlugConverter, 'uslug')
13 |
--------------------------------------------------------------------------------
/faq/snippets.py:
--------------------------------------------------------------------------------
1 | import secrets
2 | from django.conf import settings
3 |
4 | ALL_URL_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
5 |
6 |
7 | def create_random_slug(size=10):
8 | """amount of characters you want generated for random_slug"""
9 | res = ''.join(secrets.choice(ALL_URL_CHARS) for _ in range(size))
10 | return str(res)
11 |
12 |
13 | def get_template_settings(request):
14 | """returns most of the settings to be added into get_context_data method"""
15 | context = {}
16 | # if using categories
17 | if "no_category" not in settings.FAQ_SETTINGS:
18 | context['category_enabled'] = True
19 | else:
20 | context['category_enabled'] = False
21 |
22 | if "allow_multiple_answers" in settings.FAQ_SETTINGS:
23 | context['allow_multiple_answers'] = True
24 | else:
25 | context['allow_multiple_answers'] = False
26 |
27 | # if using comments
28 | if "no_comments" not in settings.FAQ_SETTINGS:
29 | context["comments_allowed"] = True
30 | if "anonymous_user_can_comment" in settings.FAQ_SETTINGS:
31 | context['add_new_comment_allowed'] = True
32 | else:
33 | if request.user.is_authenticated:
34 | context['add_new_comment_allowed'] = True
35 | else:
36 | context['add_new_comment_allowed'] = False
37 |
38 | if "view_only_comments" in settings.FAQ_SETTINGS:
39 | context['add_new_comment_allowed'] = False
40 | else:
41 | context["comments_allowed"] = False
42 |
43 | # if can vote on answers
44 | if "no_votes" not in settings.FAQ_SETTINGS:
45 | # if can vote answer
46 | if "no_answer_votes" not in settings.FAQ_SETTINGS:
47 | if request.user.is_authenticated:
48 | context["can_vote_answer"] = True
49 | else:
50 | context["can_vote_answer"] = False
51 | else:
52 |
53 | context["can_vote_answer"] = False
54 |
55 | if "no_question_votes" not in settings.FAQ_SETTINGS:
56 | if request.user.is_authenticated:
57 | context["can_vote_question"] = True
58 | else:
59 | context["can_vote_question"] = False
60 | else:
61 |
62 | context["can_vote_question"] = False
63 | else:
64 | context["can_vote_answer"] = False
65 | context["can_vote_question"] = False
66 |
67 | context["can_add_question"] = False
68 | if "logged_in_users_can_add_question" in settings.FAQ_SETTINGS:
69 | if request.user.is_authenticated:
70 | context["can_add_question"] = True
71 |
72 | return context
73 |
--------------------------------------------------------------------------------
/faq/templates/faq/answer_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Answer Question{% endblock %}
3 | {% block heading %}Answer Question{% endblock %}
4 | {% block content %}
5 | {{question.question}}
6 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/faq/templates/faq/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% block title %}FAQ{% endblock %}
6 | {% if form %}
7 | {{ form.media }}
8 | {% endif %}
9 |
10 |
11 | {% block heading %}{% endblock %}
12 |
13 | {% block content %}
14 | {% endblock %}
15 |
16 |
--------------------------------------------------------------------------------
/faq/templates/faq/categories_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Choose Category{% endblock %}
3 | {% block heading %}Select a FAQ category{% endblock %}
4 | {% block content %}
5 | {% for category in categories %}
6 |
7 | {% if category.description %}
8 | {{category.description}}
9 | {% endif %}
10 |
11 | {% endfor %}
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/faq/templates/faq/category_detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Choose a FAQ Question{% endblock %}
3 | {% block heading %}Choose a FAQ Question{% endblock %}
4 |
5 | {% block content %}
6 | {{category}}
7 | {% if category.description %}
8 | {{category.description}}
9 | {% endif %}
10 |
11 | {% for question in category.question_set.all %}
12 |
13 | {% endfor %}
14 |
15 | back
16 | {% if can_add_question %}
17 | add question
18 | {% endif %}
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/faq/templates/faq/comment_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/question_base.html' %}
2 |
3 | {% block question_content %}
4 | Post A Comment
5 | {{question.question}}
6 |
11 | {% endblock %}
--------------------------------------------------------------------------------
/faq/templates/faq/comments.html:
--------------------------------------------------------------------------------
1 | comments
2 |
3 | {% for comment in question.faqcomment_set.all %}
4 | {{comment.comment}}
5 | posted by {% if comment.user%}{{comment.user}}{% else %}anonymous{% endif %} {{comment.post_time|timesince}} ago
6 | {% endfor %}
7 |
8 | {% if add_new_comment_allowed %}
9 | {% if category_enabled %}
10 |
21 | {% endif %}
--------------------------------------------------------------------------------
/faq/templates/faq/question_base.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | {{question.question|title}}{% endblock %}
3 | {% block heading %}{{question.question|title}}{% endblock %}
4 |
5 | {% block content %}
6 | {% if can_vote_question %}
7 | found this question helpful?
8 |
13 |
18 | {% endif %}
19 | {% if question.category and category_enabled %}
20 | category - {{question.category.name}}
21 | {% endif %}
22 |
23 |
24 | {% block question_content %}
25 | {% endblock %}
26 |
27 | {% endblock %}
--------------------------------------------------------------------------------
/faq/templates/faq/question_detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/question_base.html' %}
2 |
3 | {% block question_content %}
4 | {% if allow_multiple_answers %}
5 | answers
6 |
7 | {% for answer in question.answer_set.all %}
8 |
9 | {% if answer.is_rich_text %}
10 | {{ answer.answer|safe }}
11 | {% else %}
12 | {{answer.answer}}
13 | {% endif %}
14 | {% if can_vote_answer %}
15 | | found this answer helpful?
16 |
21 |
26 | {% endif %}
27 |
28 | {% endfor %}
29 |
30 |
31 | {% else %}
32 | {% if question.answer_set.exists %}
33 | answer:
34 | {% if question.answer_set.first.is_rich_text %}
35 | {{question.answer_set.first.answer|safe}}
36 | {% else %}
37 | {{question.answer_set.first.answer}}
38 | {% endif %}
39 | {% if can_vote_answer %}
40 | found this answer helpful?
41 |
46 |
51 | {% endif %}
52 | {% else %}
53 | no answers yet
54 | {% endif %}
55 | {% endif %}
56 |
57 |
58 | {% if can_answer_question %}
59 | {% if category_enabled %}
60 | answer question
61 | {% else %}
62 | answer question
63 | {% endif %}
64 | {% endif %}
65 |
66 | {% if comments_allowed %}
67 | {% include 'faq/comments.html' %}
68 | {% endif %}
69 |
70 | {% endblock %}
--------------------------------------------------------------------------------
/faq/templates/faq/question_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Ask a Question{% endblock %}
3 | {% block heading %}Ask a Question{% endblock %}
4 | {% block content %}
5 |
10 | {% endblock %}
11 |
--------------------------------------------------------------------------------
/faq/templates/faq/questions_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Choose Question{% endblock %}
3 | {% block heading %}Choose a FAQ Question{% endblock %}
4 | {% block content %}
5 | {% for question in questions %}
6 |
7 | {% endfor %}
8 |
9 | {% if can_add_question %}
10 |
11 | add question
12 | {% endif %}
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/faq/templates/faq/vote_form.html:
--------------------------------------------------------------------------------
1 | {% extends 'faq/base.html' %}
2 | {% block title %}FAQ | Vote{% endblock %}
3 | {% block heading %}Vote{% endblock %}
4 | {% block content %}
5 |
10 | {% endblock %}
--------------------------------------------------------------------------------
/faq/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase, RequestFactory, override_settings
2 | from django.shortcuts import reverse
3 | from faq.views import IndexView, CategoryDetail, QuestionDetail
4 | from faq import models
5 | from django.contrib.auth.models import User, AnonymousUser
6 |
7 |
8 | # Create your tests here.
9 |
10 | class IndexViewTestCase(TestCase):
11 |
12 | @override_settings(FAQ_SETTINGS=["no_category"])
13 | def test_get_template_names_no_categories(self):
14 | """gets correct template when not using categories"""
15 | request = RequestFactory().get(reverse("faq:index_view"))
16 | view = IndexView()
17 | view.setup(request)
18 |
19 | self.assertEqual(view.get_template_names(), "faq/questions_list.html")
20 | self.assertNotEqual(view.get_template_names(), "faq/categories_list.html")
21 |
22 | @override_settings(FAQ_SETTINGS=[])
23 | def test_get_template_names_categories(self):
24 | """gets correct template when not using categories"""
25 | request = RequestFactory().get(reverse("faq:index_view"))
26 | view = IndexView()
27 | view.setup(request)
28 |
29 | self.assertNotEqual(view.get_template_names(), "faq/questions_list.html")
30 | self.assertEqual(view.get_template_names(), "faq/categories_list.html")
31 |
32 |
33 | @override_settings(FAQ_SETTINGS=["no_category"])
34 | def test_get_queryset_no_categories(self):
35 | """gets correct query set when not using categories"""
36 | request = RequestFactory().get(reverse("faq:index_view"))
37 | view = IndexView()
38 | view.setup(request)
39 |
40 | models.Question.objects.create(question="?")
41 | models.Question.objects.create(question="the?")
42 | view.get_queryset()
43 |
44 | self.assertEqual(view.get_queryset().first(), models.Question.objects.first())
45 | self.assertNotEqual(view.get_queryset().first(), models.Question.objects.last())
46 | self.assertNotEqual(view.get_queryset().first(), models.Category.objects.first())
47 |
48 | @override_settings(FAQ_SETTINGS=[])
49 | def test_get_queryset_categories(self):
50 | """gets correct query set when using categories"""
51 | request = RequestFactory().get(reverse("faq:index_view"))
52 | view = IndexView()
53 | view.setup(request)
54 |
55 | category = models.Category.objects.create(name="category", _description="this is a category")
56 | models.Category.objects.create(name="category 2", _description="this is a category")
57 | models.Question.objects.create(question="category question", category=category)
58 | models.Question.objects.create(question="category question 2", category=category)
59 | models.Question.objects.create(question="question not in category")
60 |
61 | self.assertEqual(view.get_queryset().first(), models.Category.objects.first())
62 | self.assertNotEqual(view.get_queryset().first(), models.Question.objects.first())
63 | self.assertNotEqual(view.get_queryset().first(), models.Category.objects.last())
64 |
65 | @override_settings(FAQ_SETTINGS=[])
66 | def test_get_context_object_name_categories(self):
67 | """gets correct template variable when using categories"""
68 | request = RequestFactory().get(reverse("faq:index_view"))
69 | view = IndexView()
70 | view.setup(request)
71 |
72 | self.assertEqual(view.get_context_object_name([]), "categories")
73 | self.assertNotEqual(view.get_context_object_name([]), "questions")
74 |
75 | @override_settings(FAQ_SETTINGS=["no_category"])
76 | def test_get_context_object_name_no_categories(self):
77 | """gets correct template variable when not using categories"""
78 | request = RequestFactory().get(reverse("faq:index_view"))
79 | view = IndexView()
80 | view.setup(request)
81 |
82 | self.assertEqual(view.get_context_object_name([]), "questions")
83 | self.assertNotEqual(view.get_context_object_name([]), "categories")
84 |
85 | @override_settings(FAQ_SETTINGS=["no_category"])
86 | def test_get_context_data_not_using_categories(self):
87 | """gets context data correctly when not using categories"""
88 | request = RequestFactory().get(reverse("faq:index_view"))
89 | request.user = AnonymousUser()
90 | view = IndexView()
91 | view.object_list = view.get_queryset()
92 | view.setup(request)
93 |
94 | self.assertIn("can_add_question", view.get_context_data())
95 |
96 | @override_settings(FAQ_SETTINGS=[])
97 | def test_get_context_data_using_categories(self):
98 | """gets context data correctly when using categories"""
99 | request = RequestFactory().get(reverse("faq:index_view"))
100 | request.user = AnonymousUser()
101 | view = IndexView()
102 | view.object_list = view.get_queryset()
103 | view.setup(request)
104 |
105 | self.assertEqual(view.get_context_data()['can_add_question'],False)
106 |
107 | @override_settings(FAQ_SETTINGS=["no_category", "logged_in_users_can_add_question"])
108 | def test_get_context_data_not_using_categories_logged_in_can_add(self):
109 | """gets context data correctly when not using categories and logged_in_users_can_add_question"""
110 | request = RequestFactory()
111 | request = request.get(reverse("faq:index_view"))
112 | request.user = User.objects.create_user(username="jim", password="the")
113 | view = IndexView()
114 | view.object_list = view.get_queryset()
115 | view.setup(request)
116 |
117 | self.assertIn("can_add_question", view.get_context_data())
118 |
119 |
120 | class CategoryDetailTestCase(TestCase):
121 | def setUp(self):
122 | models.Category.objects.create(name="cat1", _description="descript")
123 | models.Category.objects.create(name="cat2", _description="descript2")
124 | models.Category.objects.create(name="cat3")
125 |
126 |
127 | class QuestionViewTestCase(TestCase):
128 | def setUp(self):
129 | category = models.Category.objects.create(name="cat1", _description="descript")
130 |
131 | models.Question.objects.create(category=category, question="great question")
132 |
133 | @override_settings(FAQ_SETTINGS=[])
134 | def test_anonymous_user_cant_vote(self):
135 | """a user doesn't get a link to vote"""
136 |
137 | question = models.Question.objects.first()
138 | response = self.client.get(reverse("faq:question_detail", args=(question.category.slug, question.slug,)))
139 |
140 | self.assertEqual(response.context['can_vote_question'], False)
141 | self.assertEqual(response.context['can_vote_answer'], False)
142 |
143 |
144 | class VoteQuestionTestCase(TestCase):
145 | def setUp(self):
146 | category = models.Category.objects.create(name="cat1", _description="descript")
147 |
148 | models.Question.objects.create(category=category, question="great question")
149 |
150 | @override_settings(FAQ_SETTINGS=[])
151 | def test_anonymous_user_cant_vote(self):
152 | """redirects logged out users to login page"""
153 |
154 | question = models.Question.objects.first()
155 | response = self.client.post(reverse("faq:vote_question", args=(question.category.slug, question.slug,)))
156 |
157 | self.assertEqual(response.status_code, 302)
158 |
159 |
160 | class VoteanswerTestCase(TestCase):
161 | def setUp(self):
162 | category = models.Category.objects.create(name="cat1", _description="descript")
163 |
164 | question = models.Question.objects.create(category=category, question="great question")
165 |
166 | self.answer = models.Answer.objects.create(question=question, answer="because")
167 |
168 | @override_settings(FAQ_SETTINGS=[])
169 | def test_anonymous_user_cant_vote(self):
170 | """redirects logged out users to login page"""
171 |
172 | question = models.Question.objects.first()
173 | response = self.client.post(
174 | reverse("faq:vote_answer", args=(question.category.slug, question.slug, self.answer.slug)))
175 |
176 | self.assertEqual(response.status_code, 302)
177 |
--------------------------------------------------------------------------------
/faq/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 | from . import path_converters
4 | from django.conf import settings
5 | from django.contrib.auth.decorators import login_required
6 |
7 | app_name = 'faq'
8 |
9 |
10 | if "login_required" in settings.FAQ_SETTINGS:
11 | fn_decorater = login_required
12 | else:
13 | fn_decorater = lambda x: x
14 |
15 |
16 | urlpatterns = [
17 | path("", fn_decorater(views.IndexView.as_view()), name="index_view"),
18 | ]
19 |
20 | # if using categories
21 | if "no_category" not in settings.FAQ_SETTINGS:
22 | urlpatterns += [
23 | path("/",
24 | fn_decorater(views.CategoryDetail.as_view()),
25 | name="category_detail"),
26 | path("/add/question/",
27 | fn_decorater(views.AddQuestion.as_view()),
28 | name="add_question"),
29 | path("//",
30 | fn_decorater(views.QuestionDetail.as_view()),
31 | name="question_detail"),
32 | path("//answer/",
33 | fn_decorater(views.AddAnswer.as_view()),
34 | name="answer_question"),
35 | ]
36 | else:
37 | urlpatterns += [
38 | path("/",
39 | fn_decorater(views.QuestionDetail.as_view()),
40 | name="question_detail"),
41 | path("/answer/",
42 | fn_decorater(views.AddAnswer.as_view()),
43 | name="answer_question"),
44 | path("add/question",
45 | fn_decorater(views.AddQuestion.as_view()),
46 | name="add_question"),
47 | ]
48 |
49 | # if using comments
50 | if "no_comments" not in settings.FAQ_SETTINGS:
51 | # if using categories
52 | if "no_category" not in settings.FAQ_SETTINGS:
53 | urlpatterns += [
54 | path("//add/comment/",
55 | fn_decorater(views.AddComment.as_view()),
56 | name="add_comment"),
57 | ]
58 | else:
59 | urlpatterns += [
60 | path("/add/comment/",
61 | fn_decorater(views.AddComment.as_view()),
62 | name="add_comment"),
63 | ]
64 |
65 | # if using votes
66 | if "no_votes" not in settings.FAQ_SETTINGS:
67 | # if using categories
68 | if "no_category" not in settings.FAQ_SETTINGS:
69 | urlpatterns += [
70 | path("///vote/",
71 | fn_decorater(views.VoteAnswerHelpful.as_view()),
72 | name="vote_answer"),
73 | path("//vote/",
74 | fn_decorater(views.VoteQuestionHelpful.as_view()),
75 | name="vote_question")
76 | ]
77 | else:
78 | urlpatterns += [
79 | path("//vote/",
80 | fn_decorater(views.VoteAnswerHelpful.as_view()),
81 | name="vote_answer"),
82 | path("/vote/",
83 | fn_decorater(views.VoteQuestionHelpful.as_view()),
84 | name="vote_question")
85 | ]
86 |
--------------------------------------------------------------------------------
/faq/views.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import PermissionDenied
2 | from django.shortcuts import reverse
3 | from django.views import generic
4 | from django.contrib.auth.mixins import UserPassesTestMixin
5 | from django.conf import settings
6 | from . import models
7 | from . import forms
8 | from .snippets import get_template_settings
9 |
10 |
11 | # Create your views here.
12 | class IndexView(generic.ListView):
13 | """
14 | this view depending on settings either displays all the categories as a list if the categories is enabled using the categories_list.html template
15 |
16 | if categories are not enabled it will then show a list of all the questions using questions_list.html template
17 | """
18 |
19 | def get_template_names(self):
20 | """if "no_category" render questions_list.html
21 | else render categories_list.html"""
22 | if "no_category" in settings.FAQ_SETTINGS:
23 | return "faq/questions_list.html"
24 | return "faq/categories_list.html"
25 |
26 | def get_queryset(self):
27 | if "no_category" in settings.FAQ_SETTINGS:
28 | return models.Question.objects.all()
29 | return models.Category.objects.all()
30 |
31 | def get_context_object_name(self, object_list):
32 | if "no_category" in settings.FAQ_SETTINGS:
33 | return "questions"
34 | return "categories"
35 |
36 | def get_context_data(self, **kwargs):
37 | context = super().get_context_data(**kwargs)
38 | context.update(get_template_settings(self.request)) # add in all settings into templates
39 | return context
40 |
41 |
42 | class CategoryDetail(generic.DetailView):
43 | """
44 | this view only runs when categories are enabled
45 | this view shows all the questions related to this category
46 | """
47 | model = models.Category
48 | template_name = "faq/category_detail.html"
49 |
50 | def get_context_data(self, *, object_list=None, **kwargs):
51 | context = super().get_context_data()
52 | context.update(get_template_settings(self.request)) # add in all settings into templates
53 | return context
54 |
55 |
56 | class AddQuestion(UserPassesTestMixin, generic.CreateView):
57 | """
58 | this view is for a user to add a question to the faq questions
59 | if the using categories then this will also add the current category to it
60 | the default setting is only to allow the superuser to add new questions
61 | to change this:
62 | A) override the testfunc method
63 | B) set staff_can_add_question in the FAQ_SETTINGS to True (allows any staff user to add a question)
64 | C) set authenticated_user_can_add_question in the FAQ_SETTINGS to True (allows any authenticated user to add a question)
65 | """
66 | model = models.Question
67 | fields = ['question']
68 |
69 | def test_func(self):
70 | # when authenticated_user_can_add_question in the FAQ_SETTINGS is set to True
71 | if "logged_in_users_can_add_question" in settings.FAQ_SETTINGS:
72 | if self.request.user.is_authenticated:
73 | return True
74 | return False
75 |
76 | def get_success_url(self):
77 | return self.question_url
78 |
79 | def form_valid(self, form):
80 | # if using categories
81 | if "no_category" not in settings.FAQ_SETTINGS:
82 | form = form.save(commit=False)
83 | form.category = models.Category.objects.get(slug=self.kwargs["slug"])
84 | form.save()
85 | self.question_url = form.get_absolute_url()
86 | return super().form_valid(form)
87 | else:
88 | form = form.save()
89 | self.question_url = form.get_absolute_url()
90 | return super().form_valid(form)
91 |
92 | def get_context_data(self, **kwargs):
93 | context = super().get_context_data(**kwargs)
94 |
95 | if "no_category" not in settings.FAQ_SETTINGS:
96 | context['category'] = models.Category.objects.get(slug=self.kwargs["slug"])
97 | return context
98 |
99 |
100 | class QuestionDetail(generic.DetailView):
101 | model = models.Question
102 | template_name = "faq/question_detail.html"
103 | context_object_name = "question"
104 |
105 | def get_object(self, queryset=None):
106 | # if using categories
107 | if "no_category" not in settings.FAQ_SETTINGS:
108 | return self.model.objects.get(category__slug=self.kwargs["slug"], slug=self.kwargs["question"])
109 | else:
110 | return self.model.objects.get(slug=self.kwargs["slug"])
111 |
112 | def get_context_data(self, **kwargs):
113 | context = super().get_context_data()
114 | context.update(get_template_settings(self.request)) # add in all settings into templates
115 |
116 | # check if logged in users are allowed to answer questions
117 | if "logged_in_users_can_answer_question" in settings.FAQ_SETTINGS:
118 | # check if user is logged in
119 | if self.request.user.is_authenticated:
120 | if "allow_multiple_answers" in settings.FAQ_SETTINGS:
121 | context['can_answer_question'] = True
122 | else:
123 | # if there is already one answer
124 | if self.get_object().answer_set.count() > 0:
125 | context['can_answer_question'] = False
126 | else:
127 | context['can_answer_question'] = True
128 | else:
129 | context['can_answer_question'] = False
130 | else:
131 | context['can_answer_question'] = False
132 |
133 | context["comment_form"] = forms.CommentForm()
134 | return context
135 |
136 |
137 | class AddAnswer(UserPassesTestMixin, generic.CreateView):
138 | template_name = "faq/answer_form.html"
139 | form_class = forms.AnswerForm
140 |
141 | def test_func(self):
142 | # when authenticated_user_can_add_question in the FAQ_SETTINGS is set to True
143 | if "logged_in_users_can_answer_question" in settings.FAQ_SETTINGS:
144 | if self.request.user.is_authenticated:
145 | if "allow_multiple_answers" in settings.FAQ_SETTINGS:
146 | return True
147 | else:
148 |
149 | # if using categories
150 | if "no_category" not in settings.FAQ_SETTINGS:
151 | question = models.Question.objects.get(category__slug=self.kwargs['category'],
152 | slug=self.kwargs['question'])
153 | else:
154 | question = models.Question.objects.get(slug=self.kwargs['question'])
155 |
156 | # if there is already one answer don't allow user to add answer
157 | if question.answer_set.count() > 0:
158 | return False
159 | else:
160 | return True
161 | return False
162 |
163 | def get_success_url(self):
164 | # if using categories
165 | if "no_category" not in settings.FAQ_SETTINGS:
166 | return reverse("faq:question_detail", args=(self.kwargs['category'], self.kwargs['question']))
167 | else:
168 | return reverse("faq:question_detail", args=(self.kwargs['question'],))
169 |
170 | def get_context_data(self, **kwargs):
171 | context = super().get_context_data()
172 | context.update(get_template_settings(self.request)) # add in all settings into templates
173 |
174 | # if using categories
175 | if "no_category" not in settings.FAQ_SETTINGS:
176 | question = models.Question.objects.get(category__slug=self.kwargs['category'], slug=self.kwargs['question'])
177 | else:
178 | question = models.Question.objects.get(slug=self.kwargs['question'])
179 |
180 | context["question"] = question
181 | return context
182 |
183 | def form_valid(self, form):
184 | form = form.save(commit=False)
185 |
186 | # if using categories
187 | if "no_category" not in settings.FAQ_SETTINGS:
188 | question = models.Question.objects.get(category__slug=self.kwargs['category'], slug=self.kwargs['question'])
189 | else:
190 | question = models.Question.objects.get(slug=self.kwargs['question'])
191 |
192 | form.question = question
193 | if "rich_text_answers" in settings.FAQ_SETTINGS:
194 | form.is_rich_text = True
195 | form.save()
196 | return super().form_valid(form)
197 |
198 |
199 | class AddComment(generic.CreateView):
200 | model = models.FAQComment
201 | form_class = forms.CommentForm
202 | template_name = "faq/comment_form.html"
203 |
204 | def get_question(self):
205 | # if using categories
206 | if "no_category" not in settings.FAQ_SETTINGS:
207 | question = models.Question.objects.get(category__slug=self.kwargs['category'], slug=self.kwargs['question'])
208 | else:
209 | question = models.Question.objects.get(slug=self.kwargs['question'])
210 |
211 | return question
212 |
213 | def get_success_url(self):
214 | return self.get_question().get_absolute_url()
215 |
216 | def get_context_data(self, **kwargs):
217 | context = super().get_context_data()
218 | context.update(get_template_settings(self.request)) # add in all settings into templates
219 | context["question"] = self.get_question()
220 | return context
221 |
222 | def form_valid(self, form):
223 | form = form.save(commit=False)
224 | form.question = self.get_question()
225 | if self.request.user.is_authenticated:
226 | form.user = self.request.user
227 | form.save()
228 | return super().form_valid(form)
229 |
230 | def get(self, *args, **kwargs):
231 | if "view_only_comments" in settings.FAQ_SETTINGS:
232 | raise PermissionDenied("comments are view only at this time")
233 | if self.request.user.is_authenticated:
234 | pass
235 | else:
236 | if not "anonymous_user_can_comment" in settings.FAQ_SETTINGS:
237 | raise PermissionDenied("have to be logged in to comment")
238 | return super().get(*args, **kwargs)
239 |
240 | def post(self, *args, **kwargs):
241 | if "view_only_comments" in settings.FAQ_SETTINGS:
242 | raise PermissionDenied("comments are view only at this time")
243 | if self.request.user.is_authenticated:
244 | pass
245 | else:
246 | if not "anonymous_user_can_comment" in settings.FAQ_SETTINGS:
247 | raise PermissionDenied("have to be logged in to comment")
248 | return super().post(*args, **kwargs)
249 |
250 |
251 | class VoteAnswerHelpful(UserPassesTestMixin, generic.FormView):
252 | form_class = forms.VoteForm
253 | template_name = "faq/vote_form.html"
254 |
255 | def get_success_url(self):
256 | return self.get_question().get_absolute_url()
257 |
258 | def get_question(self):
259 | # if using categories
260 | if "no_category" not in settings.FAQ_SETTINGS:
261 | question = models.Question.objects.get(category__slug=self.kwargs['category'], slug=self.kwargs['question'])
262 | else:
263 | question = models.Question.objects.get(slug=self.kwargs['question'])
264 |
265 | return question
266 |
267 | def get_answer(self):
268 | return models.Answer.objects.get(question=self.get_question(), slug=self.kwargs['answer'])
269 |
270 | def form_valid(self, form):
271 | # if already voted get vote otherwise create it
272 | if models.AnswerHelpful.objects.filter(answer=self.get_answer(), user=self.request.user).exists():
273 | helpful = models.AnswerHelpful.objects.get(answer=self.get_answer(), user=self.request.user)
274 | else:
275 | helpful = models.AnswerHelpful(answer=self.get_answer(), user=self.request.user)
276 |
277 | helpful.vote = form.cleaned_data['vote']
278 | helpful.save()
279 | self.get_answer().save()
280 | return super().form_valid(form)
281 |
282 | def test_func(self):
283 | if "no_answer_votes" in settings.FAQ_SETTINGS:
284 | return False
285 | elif "no_votes" in settings.FAQ_SETTINGS:
286 | return False
287 | if not self.request.user.is_authenticated:
288 | return False
289 | return True
290 |
291 |
292 | class VoteQuestionHelpful(UserPassesTestMixin, generic.FormView):
293 | form_class = forms.VoteForm
294 | template_name = "faq/vote_form.html"
295 |
296 | def get_success_url(self):
297 | return self.get_question().get_absolute_url()
298 |
299 | def get_question(self):
300 | # if using categories
301 | if "no_category" not in settings.FAQ_SETTINGS:
302 | question = models.Question.objects.get(category__slug=self.kwargs['category'], slug=self.kwargs['question'])
303 | else:
304 | question = models.Question.objects.get(slug=self.kwargs['question'])
305 |
306 | return question
307 |
308 | def form_valid(self, form):
309 | # if already voted get vote otherwise create it
310 | if models.QuestionHelpful.objects.filter(question=self.get_question(), user=self.request.user).exists():
311 | helpful = models.QuestionHelpful.objects.get(question=self.get_question(), user=self.request.user)
312 | else:
313 | helpful = models.QuestionHelpful(question=self.get_question(), user=self.request.user)
314 |
315 | helpful.vote = form.cleaned_data['vote']
316 | helpful.save()
317 | self.get_question().save()
318 | return super().form_valid(form)
319 |
320 | def test_func(self):
321 | if "no_question_votes" in settings.FAQ_SETTINGS:
322 | return False
323 | elif "no_votes" in settings.FAQ_SETTINGS:
324 | return False
325 | if not self.request.user.is_authenticated:
326 | return False
327 | return True
328 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from setuptools import setup
4 | from os import path
5 |
6 | install_requires = [
7 | "Django>=3.2",
8 | ]
9 |
10 | here = path.abspath(path.dirname(__file__))
11 |
12 | with open(path.join(here, "README.md"), encoding="utf-8") as f:
13 | long_description = f.read()
14 |
15 | setup(
16 | name="django-easy-faq",
17 | version=os.environ.get("RELEASE_VERSION", '0.0.1'),
18 | description="A Django app to add great FAQ functionality to website",
19 | long_description=long_description,
20 | long_description_content_type="text/markdown",
21 | url="https://github.com/smark-1/django-easy-faq/",
22 | download_url="https://pypi.python.org/pypi/django-easy-faq",
23 | license="MIT",
24 | packages=["faq"],
25 | install_requires=install_requires,
26 | include_package_data=True,
27 | python_requires=">=3.9",
28 | keywords=[
29 | "django",
30 | "faq",
31 | ],
32 | classifiers=[
33 | "Intended Audience :: Developers",
34 | "License :: OSI Approved :: MIT License",
35 | "Operating System :: OS Independent",
36 | "Programming Language :: Python :: 3",
37 | "Programming Language :: Python :: 3.9",
38 | "Programming Language :: Python :: 3.10",
39 | "Programming Language :: Python :: 3.11",
40 | "Programming Language :: Python :: 3.12",
41 | "Framework :: Django",
42 | ],
43 | )
--------------------------------------------------------------------------------