Skip to content

Commit

Permalink
Add tinymce support for answers
Browse files Browse the repository at this point in the history
* Add tinymce support for answers

* Update admin for rich text
  • Loading branch information
smark-1 authored May 9, 2024
1 parent ba2351d commit a01624d
Show file tree
Hide file tree
Showing 13 changed files with 108 additions and 22 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
run: echo "RELEASE_VERSION=0.0.1" >> $GITHUB_ENV
- name: Install Dependencies
run: |
pip install django-tinymce
pip install Django
pip install -e .
- name: Run Tests
Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ add any or all to change to desired behavior::
11. no_question_votes - add if only want answer voting
12. allow_unicode - add if you want to allow unicode slugs
13. login_required - add if you want to only let logged in users see FAQ's
14. rich_text_answers - add if you want to use rich text for answers. This requires the django-tinymce package to be installed

## Templates

Expand Down Expand Up @@ -317,6 +318,24 @@ the app name for the urls is ``'faq'``
* only works if using question voting
* used to post hidden input vote = 1 or vote = 0 depending on vote up or down

## django-tinymce
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)

Make sure to include in the template the `{{ form.media }}` to include the tinymce javascript and css files.
> [!WARNING]
> Failing to follow the following steps will result in a xss vulnerability in your site.

To allow the rich text answers to be rendered properly you will need to use the safe filter in your templates.
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**.
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.
In the template you can use the following code to render the answer properly::

{% if answer.is_rich_text %}
{{answer.answer|safe}}
{% else %}
{{answer.answer}}
{% endif %}

## Contributing

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
Expand All @@ -340,4 +359,6 @@ django-easy-faq aims to be the best faq app for django. It welcomes contributio

1.6 fixed bug where no_category_description did not do remove the category description in the admin

1.7 added support for django 5.0
1.7 added support for django 5.0

1.8 added support for richtext answers with django-tinymce
10 changes: 8 additions & 2 deletions example/example/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
'faq'
'faq',
'tinymce',
]

MIDDLEWARE = [
Expand Down Expand Up @@ -150,4 +151,9 @@

# https://github.com/smark-1/django-easy-faq
# Django easy FAQ settings
FAQ_SETTINGS = []
FAQ_SETTINGS = [
'logged_in_users_can_answer_question',
'allow_multiple_answers',
'rich_text_answers',
'logged_in_users_can_add_question',
]
1 change: 1 addition & 0 deletions example/example/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
path("", home),
path('faq/', include('faq.urls')),
path("admin/", admin.site.urls),
path('tinymce/', include('tinymce.urls')),
]

24 changes: 13 additions & 11 deletions example/templates/faq/answer_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,22 @@
</nav>
{% endblock %}
{% block content %}
{{ form.media }}
<a href="{{question.get_absolute_url}}"><h2>{{question.question}}</h2></a>
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.auto_id }}" class="form-label">{{ field.label }}</label>
<input type="text" name="{{ field.name }}" class="form-control" id="{{ field.auto_id }}" required>
{% if field.errors %}
{% for error in field.errors %}
<p class="text-danger">{{ error }}</p>
{% endfor %}
{% endif %}
</div>
{% endfor %}
{{ form.as_p }}
{# {% for field in form %}#}
{# <div class="mb-3">#}
{# <label for="{{ field.auto_id }}" class="form-label">{{ field.label }}</label>#}
{# <input type="text" name="{{ field.name }}" class="form-control" id="{{ field.auto_id }}" required>#}
{# {% if field.errors %}#}
{# {% for error in field.errors %}#}
{# <p class="text-danger">{{ error }}</p>#}
{# {% endfor %}#}
{# {% endif %}#}
{# </div>#}
{# {% endfor %}#}
<input type="submit" class="btn btn-success">
</form>
{% endblock %}
11 changes: 10 additions & 1 deletion example/templates/faq/question_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ <h3 class="text-center">Answers</h3>
<div class="col-6 mb-3">
<div class="card">
<div class="card-body">
{% if answer.is_rich_text %}
<p>{{ answer.answer|safe }}</p>
{% else %}
<h5 class="card-title">{{answer.answer}}</h5>
{% endif %}
</div>
{% if can_vote_answer %}
<div class="card-footer text-body-secondary">
Expand All @@ -33,7 +37,12 @@ <h5 class="card-title">{{answer.answer}}</h5>
{% else %}
{% if question.answer_set.exists %}
<p>Answer:</p>
<h3>{{question.answer_set.first.answer}}</h3>
{% if question.answer_set.first.is_rich_text %}
<p>{{ question.answer_set.first.answer|safe }}</p>
{% else %}
<h3>{{question.answer_set.first.answer}}</h3>
{% endif %}

{% if can_vote_answer %}
found this answer helpful?
<form style="display: inline;" action="{% if category_enabled %}{% url 'faq:vote_answer' question.category.slug question.slug question.answer_set.first.slug %}{% else %}{% url 'faq:vote_answer' question.slug question.answer_set.first.slug %}{% endif %}" method="post">
Expand Down
6 changes: 3 additions & 3 deletions faq/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ class QuestionHelpfulAdmin(admin.ModelAdmin):


class AnswerAdmin(admin.ModelAdmin):
list_display = ("answer", "question", "helpful", "not_helpful")
list_filter = ('helpful', "not_helpful")
list_display = ("answer", "question", "helpful", "not_helpful",'is_rich_text')
list_filter = ('helpful', "not_helpful",'is_rich_text')
search_fields = ['answer', "question"]
readonly_fields = ('helpful', "not_helpful", 'slug')
readonly_fields = ('helpful', "not_helpful", 'slug','is_rich_text')


class CategoryAdmin(admin.ModelAdmin):
Expand Down
14 changes: 14 additions & 0 deletions faq/forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
from django import forms
from django.conf import settings
from . import models

class AnswerForm(forms.ModelForm):
class Meta:
model = models.Answer
fields = ["answer"]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if "rich_text_answers" in settings.FAQ_SETTINGS:
try:
from tinymce.widgets import TinyMCE
self.fields["answer"].widget = TinyMCE()
except ImportError:
raise ImportError("Please install django-tinymce to use rich text answers")

class CommentForm(forms.ModelForm):
class Meta:
Expand Down
18 changes: 18 additions & 0 deletions faq/migrations/0006_answer_is_rich_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-05-08 01:14

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('faq', '0005_rename_description_category__description'),
]

operations = [
migrations.AddField(
model_name='answer',
name='is_rich_text',
field=models.BooleanField(default=False),
),
]
1 change: 1 addition & 0 deletions faq/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class Answer(models.Model):
slug = models.SlugField(max_length=10, blank=True)
helpful = models.IntegerField(default=0)
not_helpful = models.IntegerField(default=0)
is_rich_text = models.BooleanField(default=False)

def get_helpful(self):
return AnswerHelpful.objects.filter(answer=self, vote=True).count()
Expand Down
3 changes: 3 additions & 0 deletions faq/templates/faq/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
<head>
<meta charset="UTF-8">
<title>{% block title %}FAQ{% endblock %}</title>
{% if form %}
{{ form.media }}
{% endif %}
</head>
<body>
<h1>{% block heading %}{% endblock %}</h1>
Expand Down
13 changes: 11 additions & 2 deletions faq/templates/faq/question_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
<h3>answers</h3>
<ul>
{% for answer in question.answer_set.all %}
<li><b>{{answer.answer}}</b>
<li>
{% if answer.is_rich_text %}
{{ answer.answer|safe }}
{% else %}
<b>{{answer.answer}}</b>
{% endif %}
{% if can_vote_answer %}
| found this answer helpful?
<form style="display: inline;" action="{% if category_enabled %}{% url 'faq:vote_answer' question.category.slug question.slug answer.slug %}{% else %}{% url 'faq:vote_answer' question.slug answer.slug %}{% endif %}" method="post">
Expand All @@ -26,7 +31,11 @@ <h3>answers</h3>
{% else %}
{% if question.answer_set.exists %}
<p>answer:</p>
<h3>{{question.answer_set.first.answer}}</h3>
{% if question.answer_set.first.is_rich_text %}
<p>{{question.answer_set.first.answer|safe}}</p>
{% else %}
<h3>{{question.answer_set.first.answer}}</h3>
{% endif %}
{% if can_vote_answer %}
found this answer helpful?
<form style="display: inline;" action="{% if category_enabled %}{% url 'faq:vote_answer' question.category.slug question.slug question.answer_set.first.slug %}{% else %}{% url 'faq:vote_answer' question.slug question.answer_set.first.slug %}{% endif %}" method="post">
Expand Down
5 changes: 3 additions & 2 deletions faq/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,8 @@ def get_context_data(self, **kwargs):


class AddAnswer(UserPassesTestMixin, generic.CreateView):
model = models.Answer
fields = ["answer"]
template_name = "faq/answer_form.html"
form_class = forms.AnswerForm

def test_func(self):
# when authenticated_user_can_add_question in the FAQ_SETTINGS is set to True
Expand Down Expand Up @@ -191,6 +190,8 @@ def form_valid(self, form):
question = models.Question.objects.get(slug=self.kwargs['question'])

form.question = question
if "rich_text_answers" in settings.FAQ_SETTINGS:
form.is_rich_text = True
form.save()
return super().form_valid(form)

Expand Down

0 comments on commit a01624d

Please sign in to comment.