Merge pull request #6326 from akatsoulas/disable-tag-creation

Do not allow the creation of new tags
This commit is contained in:
Tasos Katsoulas 2024-11-06 10:36:43 +02:00 коммит произвёл GitHub
Родитель d41cc80ce3 3d9e2cd8d8
Коммит ea918f8403
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
7 изменённых файлов: 40 добавлений и 81 удалений

Просмотреть файл

@ -387,12 +387,9 @@ class QuestionViewSet(viewsets.ModelViewSet):
try:
add_existing_tag(tag, question.tags)
except Tag.DoesNotExist:
if request.user.has_perm("taggit.add_tag"):
question.tags.add(tag)
else:
raise GenericAPIException(
status.HTTP_403_FORBIDDEN, "You are not authorized to create new tags."
)
raise GenericAPIException(
status.HTTP_403_FORBIDDEN, "You are not authorized to create new tags."
)
data = [{"name": tag.name, "slug": tag.slug} for tag in question.tags.all()]
return Response(data)

Просмотреть файл

@ -390,7 +390,7 @@
{% set tags = question.my_tags %}
{% if tags or can_tag %}
<div class="sidebox tight cf" id="tags">
<div class="tags"{% if can_tag %} data-tag-vocab-json="{{ tag_vocab() }}"{% endif %}{% if can_create_tags %} data-can-create-tags="1"{% endif %}>
<div class="tags"{% if can_tag %} data-tag-vocab-json="{{ tag_vocab() }}"{% endif %}>
{% if can_tag %}
<form action="{{ url('questions.remove_tag', question.id) }}"
data-action-async="{{ url('questions.remove_tag_async', question.id) }}"
@ -422,12 +422,12 @@
class="tag-adder">
{% csrf_token %}
<div class="field is-condensed">
<label for="id_tag_input">{{ _('Add a tag') }}:</label>
<label for="id_tag_input">{{ _('Apply a tag') }}:</label>
<input id="id_tag_input" type="text" name="tag-name" size="12" maxlength="100"
class="searchbox autocomplete-tags {% if tag_adding_error %} invalid{% endif %}"
value="{{ tag_adding_value }}" />
</div>
<input class="sumo-button button-sm primary-button" type="submit" value="{{ _('Add') }}" />
<input class="sumo-button button-sm primary-button" type="submit" value="{{ _('Apply') }}" />
</form>
{% endif %}
</div>

Просмотреть файл

@ -6,7 +6,6 @@ import actstream.actions
from actstream.models import Follow
from rest_framework.exceptions import APIException
from rest_framework.test import APIClient
from taggit.models import Tag
from kitsune.products.tests import ProductFactory, TopicFactory
from kitsune.questions import api
@ -472,23 +471,6 @@ class TestQuestionViewSet(TestCase):
self.assertEqual(res.status_code, 204)
self.assertEqual(Follow.objects.filter(user=u).count(), 0)
def test_add_tags(self):
q = QuestionFactory()
self.assertEqual(0, q.tags.count())
u = UserFactory()
add_permission(u, Tag, "add_tag")
add_permission(u, Question, "tag_question")
self.client.force_authenticate(user=u)
res = self.client.post(
reverse("question-add-tags", args=[q.id]),
content_type="application/json",
data=json.dumps({"tags": ["test", "more", "tags"]}),
)
self.assertEqual(res.status_code, 200)
self.assertEqual(3, q.tags.count())
def test_remove_tags_without_perms(self):
q = QuestionFactory()
q.tags.add("test")

Просмотреть файл

@ -971,20 +971,14 @@ class TaggingViewTestsAsAdmin(TestCase):
self.question = QuestionFactory()
TagFactory(name="red", slug="red")
def test_add_new_tag(self):
"""Assert adding a nonexistent tag sychronously creates & adds it."""
self.client.post(_add_tag_url(self.question.id), data={"tag-name": "nonexistent tag"})
tags_eq(Question.objects.get(id=self.question.id), ["nonexistent tag"])
def test_add_async_new_tag(self):
"""Assert adding an nonexistent tag creates & adds it."""
def test_add_async_new_tag_permission_error(self):
"""Assert adding an nonexistent tag returns a permission error."""
response = self.client.post(
_add_async_tag_url(self.question.id),
data={"tag-name": "nonexistent tag"},
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(response.status_code, 200)
tags_eq(Question.objects.get(id=self.question.id), ["nonexistent tag"])
self.assertEqual(response.status_code, 400)
def test_add_new_case_insensitive(self):
"""Adding a tag differing only in case from existing ones shouldn't

Просмотреть файл

@ -1432,22 +1432,14 @@ def _add_tag(request, question_id):
Tag name (case-insensitive) must be in request.POST['tag-name'].
If there is no such tag and the user is not allowed to make new tags, raise
Tag.DoesNotExist. If no tag name is provided, return None. Otherwise,
return the canonicalized tag name.
If no tag name is provided or Tag.DoesNotExist is raised, return None.
Otherwise, return the canonicalized tag name.
"""
tag_name = request.POST.get("tag-name", "").strip()
if tag_name:
if tag_name := request.POST.get("tag-name", "").strip():
question = get_object_or_404(Question, pk=question_id)
try:
canonical_name = add_existing_tag(tag_name, question.tags)
except Tag.DoesNotExist:
if request.user.has_perm("taggit.add_tag"):
question.tags.add(tag_name) # implicitly creates if needed
canonical_name = tag_name
else:
raise
# This raises Tag.DoesNotExist if the tag doesn't exist.
canonical_name = add_existing_tag(tag_name, question.tags)
return question, canonical_name

Просмотреть файл

@ -6,7 +6,7 @@ import _keys from "underscore/modules/keys";
* Scripts to support tagging.
*/
(function($) {
(function ($) {
// Initialize tagging features.
function init() {
@ -20,7 +20,7 @@ import _keys from "underscore/modules/keys";
// the .tags block surrounding each set of add and remove forms.
function initVocab() {
$('div.tags[data-tag-vocab-json]').each(
function() {
function () {
var $tagContainer = $(this);
var parsedVocab = $tagContainer.data('tag-vocab-json');
$tagContainer.data('tagVocab', _keys(parsedVocab));
@ -37,7 +37,6 @@ import _keys from "underscore/modules/keys";
var $adder = $addForm.find('input.adder'),
$input = $addForm.find('input.autocomplete-tags'),
$tagsDiv = $input.closest('div.tags'),
canCreateTags = $tagsDiv.data('can-create-tags') !== undefined,
vocab = $tagsDiv.data('tagVocab'),
$tagList = inputToTagList($input);
@ -52,7 +51,7 @@ import _keys from "underscore/modules/keys";
inVocab = inArrayCaseInsensitive(tagName, vocab) !== -1,
isOnscreen = tagIsOnscreen(tagName, $tagList);
$adder.attr('disabled', !tagName.length || isOnscreen ||
(!canCreateTags && !inVocab));
(!inVocab));
}
return tendAddButton;
@ -69,10 +68,10 @@ import _keys from "underscore/modules/keys";
function vocabCallback(request, response) {
var appliedTags = getAppliedTags($tagList),
vocabMinusApplied = $.grep(vocab,
function(e, i) {
return $.inArray(e, appliedTags) === -1;
}
);
function (e, i) {
return $.inArray(e, appliedTags) === -1;
}
);
response(filter(vocabMinusApplied, request.term));
}
@ -80,7 +79,7 @@ import _keys from "underscore/modules/keys";
}
$('input.autocomplete-tags').each(
function() {
function () {
var $input = $(this),
tender = makeButtonTender($input.closest('form'));
@ -103,11 +102,11 @@ import _keys from "underscore/modules/keys";
function initTagRemoval() {
// Attach a tag-removal function to each clickable "x":
$('div.tags').each(
function() {
function () {
var $div = $(this),
async = !$div.hasClass('deferred');
$div.find('.tag').each(
function() {
function () {
attachRemoverHandlerTo($(this), async);
}
);
@ -116,18 +115,18 @@ import _keys from "underscore/modules/keys";
// Prevent the form, if it exists, from submitting so our AJAX handler
// is always called:
$('form.remove-tag-form').on("submit", function() { return false; });
$('form.remove-tag-form').on("submit", function () { return false; });
}
// Attach onclick removal handlers to every .remove element in $tag.
function attachRemoverHandlerTo($container, async) {
$container.find('.remover').on("click",
function() {
$container.find('.remover').on("click",
function () {
var $remover = $(this),
$tag = $remover.closest('.tag'),
tagName = $tag.find('.tag-name').text(),
csrf = $remover.closest('form')
.find('input[name=csrfmiddlewaretoken]').val();
.find('input[name=csrfmiddlewaretoken]').val();
function makeTagDisappear() {
$tag.remove();
@ -140,7 +139,7 @@ import _keys from "underscore/modules/keys";
$.ajax({
type: 'POST',
url: $remover.closest('form.remove-tag-form').data('action-async'),
data: {name: tagName, csrfmiddlewaretoken: csrf},
data: { name: tagName, csrfmiddlewaretoken: csrf },
success: makeTagDisappear,
error: function makeTagReappear() {
$tag.removeClass('in-progress');
@ -201,7 +200,7 @@ import _keys from "underscore/modules/keys";
$.ajax({
type: 'POST',
url: $container.data('action-async'),
data: {'tag-name': tagName, csrfmiddlewaretoken: csrf},
data: { 'tag-name': tagName, csrfmiddlewaretoken: csrf },
success: function solidifyTag(data) {
// Make an onscreen tag non-ghostly,
// canonicalize its name,
@ -210,8 +209,8 @@ import _keys from "underscore/modules/keys";
var url = data.tagUrl,
tagNameSpan = $tag.find('.tag-name');
tagNameSpan.replaceWith($("<a class='tag-name' />")
.attr('href', url)
.text(tagNameSpan.text()));
.attr('href', url)
.text(tagNameSpan.text()));
$tag.removeClass('in-progress');
attachRemoverHandlerTo($tag, true);
},
@ -231,7 +230,7 @@ import _keys from "underscore/modules/keys";
// Dim all Add buttons. We'll undim them upon valid input.
$('div.tags input.adder:enabled').attr('disabled', true);
$('.tag-adder').each(function() {
$('.tag-adder').each(function () {
var $this = $(this),
async = !$this.hasClass('deferred');
function handler() {
@ -256,14 +255,14 @@ import _keys from "underscore/modules/keys";
// Ripped off from jquery.ui.autocomplete.js. Why can't I get at these
// via, e.g., $.ui.autocomplete.filter?
function escapeRegex( value ) {
return value.replace( /([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, '\\$1' );
function escapeRegex(value) {
return value.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, '\\$1');
}
function filter(array, term) {
var matcher = new RegExp( escapeRegex(term), 'i' );
return $.grep( array, function(value) {
return matcher.test( value.label || value.value || value );
var matcher = new RegExp(escapeRegex(term), 'i');
return $.grep(array, function (value) {
return matcher.test(value.label || value.value || value);
});
}
@ -286,7 +285,7 @@ import _keys from "underscore/modules/keys";
function getAppliedTags($tagList) {
var tagNames = [];
$tagList.find('.tag .tag-name').each(
function(i, e) {
function (i, e) {
tagNames.push($(e).text());
}
);

Просмотреть файл

@ -35,9 +35,6 @@ class TagWidget(Widget):
def make_link(self, slug):
return "#"
# Allow adding new tags to the vocab:
can_create_tags = False
# TODO: Add async_remove_url and async_add_url kwargs holding URLs to
# direct async remove and add requests to. The client app is then
# responsible for routing to those and doing the calls to remove/add
@ -88,8 +85,6 @@ class TagWidget(Widget):
if not self.read_only:
vocab = [t.name for t in Tag.objects.only("name").all()]
output += ' data-tag-vocab-json="%s"' % escape(json.dumps(vocab))
if self.can_create_tags:
output += ' data-can-create-tags="1"'
output += ">"
if not self.read_only:
@ -155,7 +150,7 @@ class TagField(MultipleChoiceField):
def valid_value(self, value):
"""Check the validity of a single tag."""
return self.widget.can_create_tags or Tag.objects.filter(name=value).exists()
return Tag.objects.filter(name=value).exists()
def to_python(self, value):
"""Ignore the input field if it's blank; don't make a tag called ''."""