зеркало из https://github.com/mozilla/kitsune.git
restrict access to documents that have no approved content (#5181)
* restrict access to documents without approved content * fix existing tests * add visibility tests * adjust based on feedback * adjust based on feedback * create common base class for Document and Revision managers
This commit is contained in:
Родитель
ae6a9fba78
Коммит
637d71f4b4
|
@ -5,7 +5,7 @@ from kitsune.kbforums.models import Post, Thread, ThreadLockedError
|
|||
from kitsune.kbforums.views import sort_threads
|
||||
from kitsune.sumo.tests import LocalizingClient, TestCase, get
|
||||
from kitsune.users.tests import UserFactory
|
||||
from kitsune.wiki.tests import DocumentFactory
|
||||
from kitsune.wiki.tests import ApprovedRevisionFactory, DocumentFactory
|
||||
|
||||
|
||||
class ThreadFactory(factory.django.DjangoModelFactory):
|
||||
|
@ -15,6 +15,12 @@ class ThreadFactory(factory.django.DjangoModelFactory):
|
|||
creator = factory.SubFactory(UserFactory)
|
||||
document = factory.SubFactory(DocumentFactory)
|
||||
|
||||
@factory.post_generation
|
||||
def add_approved_revision_to_document(obj, create, extracted, **kwargs):
|
||||
# Ensure the document has approved content, or else it'll be invisible
|
||||
# to users without special permission.
|
||||
ApprovedRevisionFactory(document=obj.document)
|
||||
|
||||
|
||||
class PostFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
|
|
|
@ -4,7 +4,7 @@ from pyquery import PyQuery as pq
|
|||
|
||||
from kitsune.kbforums.feeds import PostsFeed, ThreadsFeed
|
||||
from kitsune.kbforums.tests import KBForumTestCase, ThreadFactory, get
|
||||
from kitsune.wiki.tests import DocumentFactory
|
||||
from kitsune.wiki.tests import ApprovedRevisionFactory, DocumentFactory
|
||||
|
||||
|
||||
class FeedSortingTestCase(KBForumTestCase):
|
||||
|
@ -30,7 +30,7 @@ class FeedSortingTestCase(KBForumTestCase):
|
|||
|
||||
def test_multi_feed_titling(self):
|
||||
"""Ensure that titles are being applied properly to feeds."""
|
||||
d = DocumentFactory()
|
||||
d = ApprovedRevisionFactory().document
|
||||
response = get(self.client, "wiki.discuss.threads", args=[d.slug])
|
||||
doc = pq(response.content)
|
||||
given_ = doc('link[type="application/atom+xml"]')[0].attrib["title"]
|
||||
|
|
|
@ -10,7 +10,7 @@ from kitsune.kbforums.tests import KBForumTestCase, ThreadFactory
|
|||
from kitsune.sumo.tests import attrs_eq, post, starts_with
|
||||
from kitsune.users.models import Setting
|
||||
from kitsune.users.tests import UserFactory
|
||||
from kitsune.wiki.tests import DocumentFactory
|
||||
from kitsune.wiki.tests import ApprovedRevisionFactory, DocumentFactory
|
||||
|
||||
# Some of these contain a locale prefix on included links, while others don't.
|
||||
# This depends on whether the tests use them inside or outside the scope of a
|
||||
|
@ -75,7 +75,7 @@ class NotificationsTests(KBForumTestCase):
|
|||
@mock.patch.object(NewThreadEvent, "fire")
|
||||
def test_fire_on_new_thread(self, fire):
|
||||
"""The event fires when there is a new thread."""
|
||||
d = DocumentFactory()
|
||||
d = ApprovedRevisionFactory().document
|
||||
u = UserFactory()
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
post(
|
||||
|
@ -177,7 +177,7 @@ class NotificationsTests(KBForumTestCase):
|
|||
get_current.return_value.domain = "testserver"
|
||||
|
||||
u = UserFactory()
|
||||
d = DocumentFactory(title="an article title")
|
||||
d = ApprovedRevisionFactory(document__title="an article title").document
|
||||
f = self._toggle_watch_kbforum_as(u.username, d, turn_on=True)
|
||||
u2 = UserFactory(username="jsocol")
|
||||
self.client.login(username=u2.username, password="testpass")
|
||||
|
@ -209,7 +209,7 @@ class NotificationsTests(KBForumTestCase):
|
|||
get_current.return_value.domain = "testserver"
|
||||
|
||||
u = UserFactory()
|
||||
d = DocumentFactory()
|
||||
d = ApprovedRevisionFactory().document
|
||||
f = self._toggle_watch_kbforum_as(u.username, d, turn_on=True)
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
post(
|
||||
|
@ -227,7 +227,7 @@ class NotificationsTests(KBForumTestCase):
|
|||
get_current.return_value.domain = "testserver"
|
||||
|
||||
u = UserFactory()
|
||||
d = DocumentFactory(title="an article title")
|
||||
d = ApprovedRevisionFactory(document__title="an article title").document
|
||||
f = self._toggle_watch_kbforum_as(u.username, d, turn_on=True)
|
||||
t = ThreadFactory(title="Sticky Thread", document=d)
|
||||
u2 = UserFactory(username="jsocol")
|
||||
|
@ -253,7 +253,7 @@ class NotificationsTests(KBForumTestCase):
|
|||
get_current.return_value.domain = "testserver"
|
||||
|
||||
u = UserFactory()
|
||||
d = DocumentFactory(title="an article title")
|
||||
d = ApprovedRevisionFactory(document__title="an article title").document
|
||||
f = self._toggle_watch_kbforum_as(u.username, d, turn_on=True)
|
||||
t = ThreadFactory(document=d)
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
|
@ -267,7 +267,7 @@ class NotificationsTests(KBForumTestCase):
|
|||
get_current.return_value.domain = "testserver"
|
||||
|
||||
u = UserFactory()
|
||||
d = DocumentFactory(title="an article title")
|
||||
d = ApprovedRevisionFactory(document__title="an article title").document
|
||||
f = self._toggle_watch_kbforum_as(u.username, d, turn_on=True)
|
||||
t = ThreadFactory(title="Sticky Thread", document=d)
|
||||
self._toggle_watch_thread_as(u.username, t, turn_on=True)
|
||||
|
@ -329,7 +329,7 @@ class NotificationsTests(KBForumTestCase):
|
|||
get_current.return_value.domain = "testserver"
|
||||
|
||||
u = UserFactory()
|
||||
_d = DocumentFactory(title="an article title")
|
||||
_d = ApprovedRevisionFactory(document__title="an article title").document
|
||||
d = self._toggle_watch_kbforum_as(u.username, _d, turn_on=True)
|
||||
t = ThreadFactory(title="Sticky Thread", document=d)
|
||||
self._toggle_watch_thread_as(u.username, t, turn_on=True)
|
||||
|
@ -362,7 +362,7 @@ class NotificationsTests(KBForumTestCase):
|
|||
notify."""
|
||||
get_current.return_value.domain = "testserver"
|
||||
|
||||
d = DocumentFactory(locale="en-US")
|
||||
d = ApprovedRevisionFactory(document__locale="en-US").document
|
||||
u = UserFactory(username="berkerpeksag")
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
post(self.client, "wiki.discuss.watch_locale", {"watch": "yes"}, locale="ja")
|
||||
|
@ -384,7 +384,9 @@ class NotificationsTests(KBForumTestCase):
|
|||
"""Watching locale and create a thread."""
|
||||
get_current.return_value.domain = "testserver"
|
||||
|
||||
d = DocumentFactory(title="an article title", locale="en-US")
|
||||
d = ApprovedRevisionFactory(
|
||||
document__title="an article title", document__locale="en-US"
|
||||
).document
|
||||
u = UserFactory(username="berkerpeksag")
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
post(self.client, "wiki.discuss.watch_locale", {"watch": "yes"})
|
||||
|
@ -416,7 +418,7 @@ class NotificationsTests(KBForumTestCase):
|
|||
"""Creating a new thread should email responses"""
|
||||
get_current.return_value.domain = "testserver"
|
||||
|
||||
d = DocumentFactory()
|
||||
d = ApprovedRevisionFactory().document
|
||||
u = UserFactory()
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
s = Setting.objects.create(user=u, name="kbforums_watch_new_thread", value="False")
|
||||
|
|
|
@ -199,7 +199,7 @@ class ThreadsTemplateTests(KBForumTestCase):
|
|||
u = UserFactory()
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
|
||||
d = DocumentFactory()
|
||||
d = ApprovedRevisionFactory().document
|
||||
response = post(
|
||||
self.client,
|
||||
"wiki.discuss.new_thread",
|
||||
|
@ -217,7 +217,7 @@ class ThreadsTemplateTests(KBForumTestCase):
|
|||
u = UserFactory()
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
|
||||
d = DocumentFactory()
|
||||
d = ApprovedRevisionFactory().document
|
||||
response = post(
|
||||
self.client,
|
||||
"wiki.discuss.new_thread",
|
||||
|
@ -273,7 +273,7 @@ class ThreadsTemplateTests(KBForumTestCase):
|
|||
u = UserFactory()
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
|
||||
d = DocumentFactory()
|
||||
d = ApprovedRevisionFactory().document
|
||||
response = post(self.client, "wiki.discuss.watch_forum", {"watch": "yes"}, args=[d.slug])
|
||||
self.assertContains(response, "Stop")
|
||||
|
||||
|
@ -285,7 +285,7 @@ class ThreadsTemplateTests(KBForumTestCase):
|
|||
u = UserFactory()
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
|
||||
d = DocumentFactory()
|
||||
d = ApprovedRevisionFactory().document
|
||||
next_url = reverse("wiki.discuss.threads", args=[d.slug])
|
||||
response = post(
|
||||
self.client, "wiki.discuss.watch_locale", {"watch": "yes", "next": next_url}
|
||||
|
@ -345,7 +345,7 @@ class NewThreadTemplateTests(KBForumTestCase):
|
|||
"""Preview the thread post."""
|
||||
u = UserFactory()
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
d = DocumentFactory()
|
||||
d = ApprovedRevisionFactory().document
|
||||
num_threads = d.thread_set.count()
|
||||
content = "Full of awesome."
|
||||
response = post(
|
||||
|
@ -380,7 +380,7 @@ class FlaggedPostTests(KBForumTestCase):
|
|||
class TestRatelimiting(KBForumTestCase):
|
||||
def test_post_ratelimit(self):
|
||||
"""Verify that rate limiting kicks in after 4 threads or replies."""
|
||||
d = DocumentFactory()
|
||||
d = ApprovedRevisionFactory().document
|
||||
u = UserFactory()
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ from kitsune.kbforums.models import Thread
|
|||
from kitsune.kbforums.tests import KBForumTestCase, ThreadFactory
|
||||
from kitsune.sumo.tests import get, post
|
||||
from kitsune.users.tests import UserFactory, add_permission
|
||||
from kitsune.wiki.tests import DocumentFactory
|
||||
from kitsune.wiki.tests import ApprovedRevisionFactory, DocumentFactory
|
||||
|
||||
|
||||
class ThreadTests(KBForumTestCase):
|
||||
|
@ -14,7 +14,7 @@ class ThreadTests(KBForumTestCase):
|
|||
u = UserFactory()
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
|
||||
d = DocumentFactory()
|
||||
d = ApprovedRevisionFactory().document
|
||||
post(self.client, "wiki.discuss.watch_forum", {"watch": "yes"}, args=[d.slug])
|
||||
assert NewThreadEvent.is_notifying(u, d)
|
||||
# NewPostEvent is not notifying.
|
||||
|
@ -83,7 +83,7 @@ class ThreadTests(KBForumTestCase):
|
|||
"""If document.allow_discussion is false, should return 404."""
|
||||
u = UserFactory()
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
doc = DocumentFactory(allow_discussion=False)
|
||||
doc = ApprovedRevisionFactory(document__allow_discussion=False).document
|
||||
|
||||
def check(url):
|
||||
response = get(self.client, url, args=[doc.slug])
|
||||
|
|
|
@ -21,16 +21,16 @@ from kitsune.lib.sumo_locales import LOCALES
|
|||
from kitsune.sumo.urlresolvers import reverse
|
||||
from kitsune.sumo.utils import paginate, get_next_url, is_ratelimited
|
||||
from kitsune.users.models import Setting
|
||||
from kitsune.wiki.models import Document
|
||||
from kitsune.wiki.views import get_visible_document_or_404
|
||||
|
||||
|
||||
log = logging.getLogger("k.kbforums")
|
||||
|
||||
|
||||
def get_document(slug, request):
|
||||
"""Given a slug and a request, get the document or 404."""
|
||||
return get_object_or_404(
|
||||
Document, slug=slug, locale=request.LANGUAGE_CODE, allow_discussion=True
|
||||
"""Given a slug and a request, get the visible document or 404."""
|
||||
return get_visible_document_or_404(
|
||||
request.user, locale=request.LANGUAGE_CODE, slug=slug, allow_discussion=True
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@ class DocumentDetail(LocaleNegotiationMixin, generics.RetrieveAPIView):
|
|||
|
||||
def get_object(self):
|
||||
queryset = self.get_queryset()
|
||||
queryset = queryset.filter(locale=self.get_locale())
|
||||
queryset = queryset.filter(locale=self.get_locale(), current_revision__isnull=False)
|
||||
|
||||
obj = get_object_or_404(queryset, **self.kwargs)
|
||||
self.check_object_permissions(self.request, obj)
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
from django.db import models
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
|
||||
from kitsune.wiki.permissions import can_delete_documents_or_review_revisions
|
||||
|
||||
|
||||
class VisibilityManager(models.Manager):
|
||||
"""Abstract base class for the Document and Revision Managers."""
|
||||
|
||||
# For managers of models related to documents, provide the name of the model attribute
|
||||
# that provides the related document. For example, for the manager of revisions, this
|
||||
# should be "document".
|
||||
document_relation = None
|
||||
|
||||
def get_creator_condition(self, user):
|
||||
"""
|
||||
Return a conditional (e.g., a Q or Exists object) that is only true
|
||||
when the given user is the creator of this document or revision.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def visible(self, user, **kwargs):
|
||||
"""
|
||||
Documents are effectively invisible when they have no approved content,
|
||||
and the given user is not a superuser, nor allowed to delete documents or
|
||||
review revisions, nor a creator of one of the (yet unapproved) revisions.
|
||||
"""
|
||||
prefix = f"{self.document_relation}__" if self.document_relation else ""
|
||||
|
||||
locale = kwargs.get(f"{prefix}locale")
|
||||
|
||||
qs = self.filter(**kwargs)
|
||||
|
||||
if not user.is_authenticated:
|
||||
# Anonymous users only see documents with approved content.
|
||||
return qs.filter(**{f"{prefix}current_revision__isnull": False})
|
||||
|
||||
if not (
|
||||
user.is_superuser or can_delete_documents_or_review_revisions(user, locale=locale)
|
||||
):
|
||||
# Authenticated users without permission to see documents that
|
||||
# have no approved content, can only see those they have created.
|
||||
return qs.filter(
|
||||
Q(**{f"{prefix}current_revision__isnull": False})
|
||||
| self.get_creator_condition(user)
|
||||
)
|
||||
|
||||
return qs
|
||||
|
||||
def get_visible(self, user, **kwargs):
|
||||
return self.visible(user, **kwargs).get()
|
||||
|
||||
|
||||
class DocumentManager(VisibilityManager):
|
||||
"""The manager for the Document model."""
|
||||
|
||||
def get_creator_condition(self, user):
|
||||
from kitsune.wiki.models import Revision
|
||||
|
||||
return Exists(Revision.objects.filter(document=OuterRef("pk"), creator=user))
|
||||
|
||||
|
||||
class RevisionManager(VisibilityManager):
|
||||
"""The manager for the Revision model."""
|
||||
|
||||
document_relation = "document"
|
||||
|
||||
def get_creator_condition(self, user):
|
||||
return Q(creator=user)
|
|
@ -40,7 +40,12 @@ from kitsune.wiki.config import (
|
|||
TEMPLATES_CATEGORY,
|
||||
TYPO_SIGNIFICANCE,
|
||||
)
|
||||
from kitsune.wiki.permissions import DocumentPermissionMixin
|
||||
from kitsune.wiki.managers import DocumentManager, RevisionManager
|
||||
|
||||
from kitsune.wiki.permissions import (
|
||||
can_delete_documents_or_review_revisions,
|
||||
DocumentPermissionMixin,
|
||||
)
|
||||
|
||||
log = logging.getLogger("k.wiki")
|
||||
MAX_REVISION_COMMENT_LENGTH = 255
|
||||
|
@ -150,6 +155,8 @@ class Document(NotificationsMixin, ModelBase, BigVocabTaggableMixin, DocumentPer
|
|||
|
||||
updated_column_name = "current_revision__created"
|
||||
|
||||
objects = DocumentManager()
|
||||
|
||||
# firefox_versions,
|
||||
# operating_systems:
|
||||
# defined in the respective classes below. Use them as in
|
||||
|
@ -540,7 +547,7 @@ class Document(NotificationsMixin, ModelBase, BigVocabTaggableMixin, DocumentPer
|
|||
and not waffle.switch_is_active("hide-voting")
|
||||
)
|
||||
|
||||
def translated_to(self, locale):
|
||||
def translated_to(self, locale, visible_for_user=None):
|
||||
"""Return the translation of me to the given locale.
|
||||
|
||||
If there is no such Document, return None.
|
||||
|
@ -553,6 +560,8 @@ class Document(NotificationsMixin, ModelBase, BigVocabTaggableMixin, DocumentPer
|
|||
"far."
|
||||
)
|
||||
try:
|
||||
if visible_for_user:
|
||||
return Document.objects.get_visible(visible_for_user, locale=locale, parent=self)
|
||||
return Document.objects.get(locale=locale, parent=self)
|
||||
except Document.DoesNotExist:
|
||||
return None
|
||||
|
@ -711,6 +720,25 @@ class Document(NotificationsMixin, ModelBase, BigVocabTaggableMixin, DocumentPer
|
|||
# Clear out both mobile and desktop templates.
|
||||
cache.delete(doc_html_cache_key(self.locale, self.slug))
|
||||
|
||||
def is_visible_for(self, user):
|
||||
"""
|
||||
This document is effectively invisible when it has no approved content,
|
||||
and the given user is not a superuser, nor allowed to delete documents or
|
||||
review revisions, nor a creator of one of the document's (yet unapproved)
|
||||
revisions.
|
||||
"""
|
||||
return (
|
||||
self.current_revision
|
||||
or user.is_superuser
|
||||
or (
|
||||
user.is_authenticated
|
||||
and (
|
||||
can_delete_documents_or_review_revisions(user, locale=self.locale)
|
||||
or self.revisions.filter(creator=user).exists()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class AbstractRevision(models.Model):
|
||||
# **%(class)s** is being used because it will allow a unique reverse name for the field
|
||||
|
@ -764,6 +792,8 @@ class Revision(ModelBase, AbstractRevision):
|
|||
User, on_delete=models.CASCADE, related_name="readied_for_l10n_revisions", null=True
|
||||
)
|
||||
|
||||
objects = RevisionManager()
|
||||
|
||||
class Meta(object):
|
||||
permissions = [
|
||||
("review_revision", "Can review a revision"),
|
||||
|
|
|
@ -130,3 +130,16 @@ def _is_reviewer(locale, user):
|
|||
return False
|
||||
|
||||
return user in locale_team.reviewers.all()
|
||||
|
||||
|
||||
def can_delete_documents_or_review_revisions(user, locale=None):
|
||||
"""
|
||||
Can the given user delete documents or review revisions. If an optional locale is
|
||||
provided, will perform the extra check of whether the user is a leader or reviewer
|
||||
within that locale team.
|
||||
"""
|
||||
if locale and (_is_leader(locale, user) or _is_reviewer(locale, user)):
|
||||
return True
|
||||
|
||||
# Fallback to the django permissions.
|
||||
return user.has_perm("wiki.review_revision") or user.has_perm("wiki.delete_document")
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
from kitsune.sumo.tests import TestCase
|
||||
from kitsune.sumo.urlresolvers import reverse
|
||||
from kitsune.wiki.tests import ApprovedRevisionFactory, DocumentFactory
|
||||
|
||||
|
||||
class TestDocumentListView(TestCase):
|
||||
def test_it_works(self):
|
||||
# Create two documents, one with approved content, and one without.
|
||||
doc1 = DocumentFactory()
|
||||
doc2 = ApprovedRevisionFactory().document
|
||||
url = reverse("document-list")
|
||||
res = self.client.get(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
# Only the document with approved content should be present.
|
||||
self.assertNotContains(res, doc1.slug)
|
||||
self.assertNotContains(res, doc1.title)
|
||||
self.assertContains(res, doc2.slug, count=1)
|
||||
self.assertContains(res, doc2.title, count=1)
|
||||
|
|
|
@ -30,7 +30,7 @@ from kitsune.wiki.events import (
|
|||
ReviewableRevisionInLocaleEvent,
|
||||
get_diff_for,
|
||||
)
|
||||
from kitsune.wiki.models import Document, HelpfulVote, HelpfulVoteMetadata, Revision
|
||||
from kitsune.wiki.models import Document, HelpfulVote, HelpfulVoteMetadata, Locale, Revision
|
||||
from kitsune.wiki.tasks import send_reviewed_notification
|
||||
from kitsune.wiki.tests import (
|
||||
ApprovedRevisionFactory,
|
||||
|
@ -139,6 +139,9 @@ class DocumentTests(TestCaseBase):
|
|||
|
||||
def test_english_document_no_approved_content(self):
|
||||
"""Load an English document with no approved content."""
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
r = RevisionFactory(content="Some text.", is_approved=False)
|
||||
response = self.client.get(r.document.get_absolute_url())
|
||||
self.assertEqual(200, response.status_code)
|
||||
|
@ -152,6 +155,9 @@ class DocumentTests(TestCaseBase):
|
|||
def test_translation_document_no_approved_content(self):
|
||||
"""Load a non-English document with no approved content, with a parent
|
||||
with no approved content either."""
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
r = RevisionFactory(content="Some text.", is_approved=False)
|
||||
d2 = DocumentFactory(parent=r.document, locale="fr", slug="french")
|
||||
RevisionFactory(document=d2, content="Moartext", is_approved=False)
|
||||
|
@ -166,6 +172,9 @@ class DocumentTests(TestCaseBase):
|
|||
def test_document_fallback_with_translation(self):
|
||||
"""The document template falls back to English if translation exists
|
||||
but it has no approved revisions."""
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
r = ApprovedRevisionFactory(content="Test")
|
||||
d2 = DocumentFactory(parent=r.document, locale="fr", slug="french")
|
||||
RevisionFactory(document=d2, is_approved=False)
|
||||
|
@ -185,6 +194,9 @@ class DocumentTests(TestCaseBase):
|
|||
def test_document_fallback_with_translation_english_slug(self):
|
||||
"""The document template falls back to English if translation exists
|
||||
but it has no approved revisions, while visiting the English slug."""
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
r = ApprovedRevisionFactory(content="Test")
|
||||
d2 = DocumentFactory(parent=r.document, locale="fr", slug="french")
|
||||
RevisionFactory(document=d2, is_approved=False)
|
||||
|
@ -192,6 +204,7 @@ class DocumentTests(TestCaseBase):
|
|||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual("/fr/kb/french", response.redirect_chain[0][0])
|
||||
doc = pq(response.content)
|
||||
self.assertEqual(d2.title, doc("h1.sumo-page-heading").text())
|
||||
# Fallback message is shown.
|
||||
self.assertEqual(1, len(doc("#doc-pending-fallback")))
|
||||
# Removing this as it shows up in text(), and we don't want to depend
|
||||
|
@ -200,6 +213,13 @@ class DocumentTests(TestCaseBase):
|
|||
# Included content is English.
|
||||
self.assertEqual(pq(r.document.html).text(), doc("#doc-content").text())
|
||||
|
||||
self.client.logout()
|
||||
# Users without permission to see unapproved documents will see the
|
||||
# English document's title.
|
||||
response = self.client.get(url)
|
||||
doc = pq(response.content)
|
||||
self.assertEqual(r.document.title, doc("h1.sumo-page-heading").text())
|
||||
|
||||
def test_document_fallback_no_translation(self):
|
||||
"""The document template falls back to English if no translation exists."""
|
||||
r = ApprovedRevisionFactory(content="Some text.")
|
||||
|
@ -240,6 +260,9 @@ class DocumentTests(TestCaseBase):
|
|||
Also check the backlink to the redirect page.
|
||||
|
||||
"""
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
target = DocumentFactory()
|
||||
target_url = target.get_absolute_url()
|
||||
|
||||
|
@ -261,6 +284,9 @@ class DocumentTests(TestCaseBase):
|
|||
|
||||
def test_redirect_no_vote(self):
|
||||
"""Make sure documents with REDIRECT directives have no vote form."""
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
target = DocumentFactory()
|
||||
redirect = RedirectRevisionFactory(target=target).document
|
||||
redirect_url = redirect.get_absolute_url()
|
||||
|
@ -271,6 +297,9 @@ class DocumentTests(TestCaseBase):
|
|||
def test_redirect_from_nonexistent(self):
|
||||
"""The template shouldn't crash or print a backlink if the "from" page
|
||||
doesn't exist."""
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
d = DocumentFactory()
|
||||
response = self.client.get(
|
||||
urlparams(d.get_absolute_url(), redirectlocale="en-US", redirectslug="nonexistent")
|
||||
|
@ -279,8 +308,9 @@ class DocumentTests(TestCaseBase):
|
|||
|
||||
def test_watch_includes_csrf(self):
|
||||
"""The watch/unwatch forms should include the csrf tag."""
|
||||
u = UserFactory()
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
d = DocumentFactory()
|
||||
resp = self.client.get(d.get_absolute_url())
|
||||
doc = pq(resp.content)
|
||||
|
@ -288,8 +318,9 @@ class DocumentTests(TestCaseBase):
|
|||
|
||||
def test_non_localizable_translate_disabled(self):
|
||||
"""Non localizable document doesn't show tab for 'Localize'."""
|
||||
u = UserFactory()
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
d = DocumentFactory(is_localizable=True)
|
||||
resp = self.client.get(d.get_absolute_url())
|
||||
doc = pq(resp.content)
|
||||
|
@ -304,6 +335,9 @@ class DocumentTests(TestCaseBase):
|
|||
|
||||
def test_obsolete_hide_edit(self):
|
||||
"""Make sure Edit sidebar link is hidden for obsolete articles."""
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
d = DocumentFactory(is_archived=True)
|
||||
r = self.client.get(d.get_absolute_url())
|
||||
doc = pq(r.content)
|
||||
|
@ -413,7 +447,7 @@ class DocumentTests(TestCaseBase):
|
|||
exists."""
|
||||
u = UserFactory()
|
||||
self.client.login(username=u.username, password="testpass")
|
||||
# Create an English document and a es translated document
|
||||
# Create an English document and an es translated document
|
||||
en_rev = ApprovedRevisionFactory(is_ready_for_localization=True)
|
||||
trans_doc = DocumentFactory(parent=en_rev.document, locale="es")
|
||||
trans_rev = ApprovedRevisionFactory(document=trans_doc)
|
||||
|
@ -567,7 +601,8 @@ class RevisionTests(TestCaseBase):
|
|||
def test_mark_as_ready_no_approval(self, fire):
|
||||
"""Mark an unapproved revision as ready for l10n must fail."""
|
||||
|
||||
r = RevisionFactory(is_approved=False, is_ready_for_localization=False)
|
||||
doc = ApprovedRevisionFactory().document
|
||||
r = RevisionFactory(document=doc, is_approved=False, is_ready_for_localization=False)
|
||||
|
||||
u = UserFactory()
|
||||
add_permission(u, Revision, "mark_ready_for_l10n")
|
||||
|
@ -892,6 +927,10 @@ class NewRevisionTests(TestCaseBase):
|
|||
the document fields are open for editing.
|
||||
|
||||
"""
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
|
||||
get_current.return_value.domain = "testserver"
|
||||
|
||||
self.d.current_revision = None
|
||||
|
@ -911,6 +950,9 @@ class NewRevisionTests(TestCaseBase):
|
|||
def test_edit_document_POST_removes_old_tags(self):
|
||||
"""Changing the tags on a document removes the old tags from
|
||||
that document."""
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
self.d.current_revision = None
|
||||
self.d.save()
|
||||
topics = [TopicFactory(), TopicFactory(), TopicFactory()]
|
||||
|
@ -1166,6 +1208,9 @@ class HistoryTests(TestCaseBase):
|
|||
|
||||
def test_translation_history_with_english_slug(self):
|
||||
"""Request in en-US slug but translated locale should redirect to translation history"""
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
doc = DocumentFactory(locale=settings.WIKI_DEFAULT_LANGUAGE)
|
||||
trans = DocumentFactory(parent=doc, locale="bn", slug="bn_trans_slug")
|
||||
ApprovedRevisionFactory(document=trans)
|
||||
|
@ -1179,6 +1224,9 @@ class HistoryTests(TestCaseBase):
|
|||
|
||||
def test_translation_history_with_english_slug_while_no_trans(self):
|
||||
"""Request in en-US slug but untranslated locale should raise 404"""
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
doc = DocumentFactory(locale=settings.WIKI_DEFAULT_LANGUAGE)
|
||||
url = reverse("wiki.document_revisions", args=[doc.slug], locale="bn")
|
||||
response = self.client.get(url)
|
||||
|
@ -2184,6 +2232,9 @@ class TranslateTests(TestCaseBase):
|
|||
def test_translate_rejected_parent(self):
|
||||
"""Translate view of rejected English document shows warning."""
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
user = UserFactory()
|
||||
en_revision = RevisionFactory(is_approved=False, reviewer=user, reviewed=datetime.now())
|
||||
|
||||
url = reverse("wiki.translate", locale="es", args=[en_revision.document.slug])
|
||||
|
@ -2255,6 +2306,9 @@ class TranslateTests(TestCaseBase):
|
|||
self.assertEqual(r.id, new_es_rev.based_on_id)
|
||||
|
||||
def test_show_translations_page(self):
|
||||
user = UserFactory()
|
||||
add_permission(user, Revision, "review_revision")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
en = settings.WIKI_DEFAULT_LANGUAGE
|
||||
en_doc = DocumentFactory(locale=en, slug="english-slug")
|
||||
DocumentFactory(locale="de", parent=en_doc)
|
||||
|
@ -2731,7 +2785,7 @@ class RevisionDeleteTestCase(TestCaseBase):
|
|||
|
||||
# Create document with only 1 revision
|
||||
doc = DocumentFactory()
|
||||
rev = RevisionFactory(document=doc)
|
||||
rev = ApprovedRevisionFactory(document=doc)
|
||||
|
||||
# Confirm page should show the message
|
||||
response = get(self.client, "wiki.delete_revision", args=[doc.slug, rev.id])
|
||||
|
@ -2791,6 +2845,7 @@ class DocumentDeleteTestCase(TestCaseBase):
|
|||
|
||||
def test_delete_document_without_permissions(self):
|
||||
"""Deleting a document without permissions sends 403."""
|
||||
ApprovedRevisionFactory(document=self.document)
|
||||
self.client.login(username="testuser", password="testpass")
|
||||
response = get(self.client, "wiki.document_delete", args=[self.document.slug])
|
||||
self.assertEqual(403, response.status_code)
|
||||
|
@ -2863,6 +2918,11 @@ class RecentRevisionsTest(TestCaseBase):
|
|||
_create_document(title="4", locale="fr", rev_kwargs={"creator": self.u2})
|
||||
_create_document(title="5", locale="fr", rev_kwargs={"creator": self.u2})
|
||||
|
||||
# Create a document without any approved content for visibility testing.
|
||||
RevisionFactory(
|
||||
is_approved=False, creator=self.u2, document__title="6", document__locale="fr"
|
||||
)
|
||||
|
||||
self.url = reverse("wiki.revisions")
|
||||
|
||||
def test_basic(self):
|
||||
|
@ -2919,6 +2979,47 @@ class RecentRevisionsTest(TestCaseBase):
|
|||
doc = pq(res.content)
|
||||
self.assertEqual(len(doc("#revisions-fragment ul li:not(.header)")), 1)
|
||||
|
||||
def test_visibility(self):
|
||||
"""
|
||||
Test that revisions of documents without any approved content are visible
|
||||
only to their creators, superusers, or users with one of a set of permissions.
|
||||
"""
|
||||
with self.subTest("creator"):
|
||||
self.client.login(username=self.u2.username, password="testpass")
|
||||
res = self.client.get(self.url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
doc = pq(res.content)
|
||||
self.assertEqual(len(doc("#revisions-fragment ul li:not(.header)")), 6)
|
||||
self.client.logout()
|
||||
|
||||
for perm in ("superuser", "review_revision", "delete_document"):
|
||||
with self.subTest(perm):
|
||||
user = UserFactory(is_superuser=(perm == "superuser"))
|
||||
if perm == "review_revision":
|
||||
add_permission(user, Revision, "review_revision")
|
||||
elif perm == "delete_document":
|
||||
add_permission(user, Document, "delete_document")
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
res = self.client.get(self.url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
doc = pq(res.content)
|
||||
self.assertEqual(len(doc("#revisions-fragment ul li:not(.header)")), 6)
|
||||
self.client.logout()
|
||||
|
||||
for perm in ("fr__leaders", "fr__reviewers"):
|
||||
with self.subTest(perm):
|
||||
user = UserFactory()
|
||||
locale, role = perm.split("__")
|
||||
locale_team, _ = Locale.objects.get_or_create(locale=locale)
|
||||
getattr(locale_team, role).add(user)
|
||||
self.client.login(username=user.username, password="testpass")
|
||||
url = urlparams(self.url, locale="fr")
|
||||
res = self.client.get(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
doc = pq(res.content)
|
||||
self.assertEqual(len(doc("#revisions-fragment ul li:not(.header)")), 3)
|
||||
self.client.logout()
|
||||
|
||||
|
||||
# TODO: This should be a factory subclass
|
||||
def _create_document(
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -4,6 +4,7 @@ import requests
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Prefetch, Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from kitsune.dashboards import LAST_7_DAYS
|
||||
|
@ -174,3 +175,11 @@ def get_featured_articles(product=None, locale=settings.WIKI_DEFAULT_LANGUAGE):
|
|||
if len(documents) <= 4:
|
||||
return documents
|
||||
return random.sample(documents, 4)
|
||||
|
||||
|
||||
def get_visible_document_or_404(user, **kwargs):
|
||||
return get_object_or_404(Document.objects.visible(user, **kwargs))
|
||||
|
||||
|
||||
def get_visible_revision_or_404(user, **kwargs):
|
||||
return get_object_or_404(Revision.objects.visible(user, **kwargs))
|
||||
|
|
|
@ -67,6 +67,8 @@ from kitsune.wiki.tasks import (
|
|||
send_contributor_notification,
|
||||
send_reviewed_notification,
|
||||
)
|
||||
from kitsune.wiki.utils import get_visible_document_or_404, get_visible_revision_or_404
|
||||
|
||||
|
||||
log = logging.getLogger("k.wiki")
|
||||
|
||||
|
@ -118,55 +120,56 @@ def document(request, document_slug, document=None):
|
|||
full_locale_name = None
|
||||
# If a slug isn't available in the requested locale, fall back to en-US:
|
||||
try:
|
||||
doc = Document.objects.get(locale=request.LANGUAGE_CODE, slug=document_slug)
|
||||
doc = Document.objects.get_visible(
|
||||
request.user, locale=request.LANGUAGE_CODE, slug=document_slug
|
||||
)
|
||||
if not doc.current_revision and doc.parent and doc.parent.current_revision:
|
||||
# This is a translation but its current_revision is None
|
||||
# and OK to fall back to parent (parent is approved).
|
||||
fallback_reason = "translation_not_approved"
|
||||
elif not doc.current_revision:
|
||||
# No current_revision, no parent with current revision, so
|
||||
# nothing to show.
|
||||
# The document as well as its parent have no approved content, so we won't
|
||||
# show any content at all.
|
||||
fallback_reason = "no_content"
|
||||
except Document.DoesNotExist:
|
||||
# Look in default language:
|
||||
doc = get_object_or_404(
|
||||
Document, locale=settings.WIKI_DEFAULT_LANGUAGE, slug=document_slug
|
||||
if request.LANGUAGE_CODE == settings.WIKI_DEFAULT_LANGUAGE:
|
||||
# Don't repeat the query if it's going to be the same one we just tried.
|
||||
raise Http404
|
||||
# No visible document exists in the requested locale so let's try the default locale.
|
||||
doc = get_visible_document_or_404(
|
||||
request.user, locale=settings.WIKI_DEFAULT_LANGUAGE, slug=document_slug
|
||||
)
|
||||
# If there's a translation to the requested locale, take it:
|
||||
translation = doc.translated_to(request.LANGUAGE_CODE)
|
||||
# If there's a visible translation to the requested locale, redirect to it.
|
||||
translation = doc.translated_to(request.LANGUAGE_CODE, visible_for_user=request.user)
|
||||
if translation:
|
||||
url = translation.get_absolute_url()
|
||||
url = urlparams(url, query_dict=request.GET)
|
||||
return HttpResponseRedirect(url)
|
||||
elif doc.current_revision:
|
||||
# There is no translation
|
||||
# and OK to fall back to parent (parent is approved).
|
||||
# There is no translation, so we'll fall back to the approved parent,
|
||||
# unless we find an approved translation in a fallback locale.
|
||||
fallback_reason = "no_translation"
|
||||
|
||||
# Find and show the defined fallback locale rather than the English version of the document
|
||||
# The fallback locale is defined based on the ACCEPT_LANGUAGE header,
|
||||
# site-wide locale mapping and custom fallback locale
|
||||
# The custom fallback locale is defined in the FALLBACK_LOCALES array in
|
||||
# kitsune/wiki/config.py. See bug 800880 for more details
|
||||
if fallback_reason == "no_translation":
|
||||
fallback_locale = get_fallback_locale(doc, request)
|
||||
# Find and show the defined fallback locale rather than the English
|
||||
# version of the document. The fallback locale is defined based on
|
||||
# the ACCEPT_LANGUAGE header, site-wide locale mapping and custom
|
||||
# fallback locale. The custom fallback locale is defined in the
|
||||
# FALLBACK_LOCALES array in kitsune/wiki/config.py. See bug 800880
|
||||
# for more details
|
||||
fallback_locale = get_fallback_locale(doc, request)
|
||||
|
||||
# If a fallback locale is defined, show the document in that locale.
|
||||
if fallback_locale is not None:
|
||||
# Get the fallback Locale and show doc in the locale
|
||||
translation = doc.translated_to(fallback_locale)
|
||||
doc = translation
|
||||
# For showing the fallback locale explanation message to the user
|
||||
fallback_reason = "fallback_locale"
|
||||
full_locale_name = {
|
||||
request.LANGUAGE_CODE: LOCALES[request.LANGUAGE_CODE].native,
|
||||
fallback_locale: LOCALES[fallback_locale].native,
|
||||
}
|
||||
# If there is no defined fallback locale, show the document in English
|
||||
else:
|
||||
doc = get_object_or_404(
|
||||
Document, locale=settings.WIKI_DEFAULT_LANGUAGE, slug=document_slug
|
||||
)
|
||||
# If a fallback locale is defined, show the document in that locale,
|
||||
# otherwise continue with the document in the default language.
|
||||
if fallback_locale:
|
||||
# If we have a fallback locale, we're certain to have an approved
|
||||
# translation in that locale.
|
||||
doc = doc.translated_to(fallback_locale)
|
||||
# For showing the fallback locale explanation message to the user
|
||||
fallback_reason = "fallback_locale"
|
||||
full_locale_name = {
|
||||
request.LANGUAGE_CODE: LOCALES[request.LANGUAGE_CODE].native,
|
||||
fallback_locale: LOCALES[fallback_locale].native,
|
||||
}
|
||||
|
||||
any_localizable_revision = doc.revisions.filter(
|
||||
is_approved=True, is_ready_for_localization=True
|
||||
|
@ -271,15 +274,23 @@ def document(request, document_slug, document=None):
|
|||
|
||||
def revision(request, document_slug, revision_id):
|
||||
"""View a wiki document revision."""
|
||||
rev = get_object_or_404(Revision, pk=revision_id, document__slug=document_slug)
|
||||
data = {"document": rev.document, "revision": rev}
|
||||
rev = get_visible_revision_or_404(
|
||||
request.user,
|
||||
pk=revision_id,
|
||||
document__slug=document_slug,
|
||||
document__locale=request.LANGUAGE_CODE,
|
||||
)
|
||||
doc = rev.document
|
||||
data = {"document": doc, "revision": rev}
|
||||
return render(request, "wiki/revision.html", data)
|
||||
|
||||
|
||||
@require_GET
|
||||
def list_documents(request, category=None):
|
||||
"""List wiki documents."""
|
||||
docs = Document.objects.filter(locale=request.LANGUAGE_CODE).order_by("title")
|
||||
user = request.user
|
||||
docs = Document.objects.visible(user, locale=request.LANGUAGE_CODE).order_by("title")
|
||||
|
||||
if category:
|
||||
docs = docs.filter(category=category)
|
||||
try:
|
||||
|
@ -415,9 +426,8 @@ def _document_lock(doc_id, username):
|
|||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def steal_lock(request, document_slug, revision_id=None):
|
||||
doc = get_object_or_404(Document, locale=request.LANGUAGE_CODE, slug=document_slug)
|
||||
user = request.user
|
||||
|
||||
doc = get_visible_document_or_404(user, locale=request.LANGUAGE_CODE, slug=document_slug)
|
||||
ok = _document_lock_steal(doc.id, user.username)
|
||||
return HttpResponse("", status=200 if ok else 400)
|
||||
|
||||
|
@ -426,28 +436,31 @@ def steal_lock(request, document_slug, revision_id=None):
|
|||
@login_required
|
||||
def edit_document(request, document_slug, revision_id=None):
|
||||
"""Create a new revision of a wiki document, or edit document metadata."""
|
||||
user = request.user
|
||||
|
||||
try:
|
||||
doc = Document.objects.get(locale=request.LANGUAGE_CODE, slug=document_slug)
|
||||
doc = Document.objects.get_visible(user, locale=request.LANGUAGE_CODE, slug=document_slug)
|
||||
except Document.DoesNotExist:
|
||||
# Check if the document slug is available in default language.
|
||||
parent_doc = get_object_or_404(
|
||||
Document, locale=settings.WIKI_DEFAULT_LANGUAGE, slug=document_slug
|
||||
if request.LANGUAGE_CODE == settings.WIKI_DEFAULT_LANGUAGE:
|
||||
# Don't repeat the query if it's going to be the same one we just tried.
|
||||
raise Http404
|
||||
# Check if the document slug is available in the default language.
|
||||
parent_doc = get_visible_document_or_404(
|
||||
user, locale=settings.WIKI_DEFAULT_LANGUAGE, slug=document_slug
|
||||
)
|
||||
# If the document is available in default language, show the user the translation page
|
||||
# of the requested locale
|
||||
translation = parent_doc.translated_to(request.LANGUAGE_CODE)
|
||||
# If the document is translated into the requested locale, show them the edit article
|
||||
# page of that translated document
|
||||
# We've found the parent using the given slug, so let's see if there's a translation
|
||||
# in the requested locale that's using a different slug.
|
||||
translation = parent_doc.translated_to(request.LANGUAGE_CODE, visible_for_user=user)
|
||||
if translation:
|
||||
# The document is translated into the requested locale, so show them the edit
|
||||
# article page of the translated document.
|
||||
doc = translation
|
||||
# If the document is not translated into the requested locale, redirect them to translate
|
||||
# the article page.
|
||||
else:
|
||||
# The document is not translated into the requested locale, so redirect them to
|
||||
# translate the article page.
|
||||
url = reverse("wiki.translate", locale=request.LANGUAGE_CODE, args=[document_slug])
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
user = request.user
|
||||
|
||||
can_edit_needs_change = doc.allows(user, "edit_needs_change")
|
||||
can_archive = doc.allows(user, "archive")
|
||||
|
||||
|
@ -580,7 +593,7 @@ def preview_revision(request):
|
|||
locale = request.POST.get("locale")
|
||||
|
||||
if slug and locale:
|
||||
doc = get_object_or_404(Document, slug=slug, locale=locale)
|
||||
doc = get_visible_document_or_404(request.user, locale=locale, slug=slug)
|
||||
products = doc.get_products()
|
||||
else:
|
||||
products = Product.objects.all()
|
||||
|
@ -594,15 +607,18 @@ def document_revisions(request, document_slug, contributor_form=None):
|
|||
"""List all the revisions of a given document."""
|
||||
locale = request.GET.get("locale", request.LANGUAGE_CODE)
|
||||
try:
|
||||
doc = Document.objects.get(locale=locale, slug=document_slug)
|
||||
doc = Document.objects.get_visible(request.user, locale=locale, slug=document_slug)
|
||||
except Document.DoesNotExist:
|
||||
if locale == settings.WIKI_DEFAULT_LANGUAGE:
|
||||
# Don't repeat the query if it's going to be the same one we just tried.
|
||||
raise Http404
|
||||
# Check if the document slug is available in default language.
|
||||
parent_doc = get_object_or_404(
|
||||
Document, locale=settings.WIKI_DEFAULT_LANGUAGE, slug=document_slug
|
||||
parent_doc = get_visible_document_or_404(
|
||||
request.user, locale=settings.WIKI_DEFAULT_LANGUAGE, slug=document_slug
|
||||
)
|
||||
# If the document is available in default language, show the user the history page
|
||||
# of the requested locale
|
||||
translation = parent_doc.translated_to(locale)
|
||||
# of the requested locale.
|
||||
translation = parent_doc.translated_to(locale, visible_for_user=request.user)
|
||||
if translation:
|
||||
url = reverse("wiki.document_revisions", args=[translation.slug], locale=locale)
|
||||
return HttpResponseRedirect(url)
|
||||
|
@ -769,7 +785,7 @@ def compare_revisions(request, document_slug):
|
|||
|
||||
"""
|
||||
locale = request.GET.get("locale", request.LANGUAGE_CODE)
|
||||
doc = get_object_or_404(Document, locale=locale, slug=document_slug)
|
||||
doc = get_visible_document_or_404(request.user, locale=locale, slug=document_slug)
|
||||
if "from" not in request.GET or "to" not in request.GET:
|
||||
raise Http404
|
||||
|
||||
|
@ -793,7 +809,9 @@ def compare_revisions(request, document_slug):
|
|||
@login_required
|
||||
def select_locale(request, document_slug):
|
||||
"""Select a locale to translate the document to."""
|
||||
doc = get_object_or_404(Document, locale=settings.WIKI_DEFAULT_LANGUAGE, slug=document_slug)
|
||||
doc = get_visible_document_or_404(
|
||||
request.user, locale=settings.WIKI_DEFAULT_LANGUAGE, slug=document_slug
|
||||
)
|
||||
translated_locales_code = [] # Translated Locales list with Locale Code only
|
||||
translated_locales = []
|
||||
untranslated_locales = []
|
||||
|
@ -829,10 +847,10 @@ def translate(request, document_slug, revision_id=None):
|
|||
"""
|
||||
|
||||
# Inialization and checks
|
||||
parent_doc = get_object_or_404(
|
||||
Document, locale=settings.WIKI_DEFAULT_LANGUAGE, slug=document_slug
|
||||
)
|
||||
user = request.user
|
||||
parent_doc = get_visible_document_or_404(
|
||||
user, locale=settings.WIKI_DEFAULT_LANGUAGE, slug=document_slug
|
||||
)
|
||||
|
||||
if settings.WIKI_DEFAULT_LANGUAGE == request.LANGUAGE_CODE:
|
||||
# Don't translate to the default language.
|
||||
|
@ -855,6 +873,12 @@ def translate(request, document_slug, revision_id=None):
|
|||
except Document.DoesNotExist:
|
||||
doc = None
|
||||
disclose_description = True
|
||||
else:
|
||||
if not doc.is_visible_for(user):
|
||||
# A translation has been started, but isn't approved yet for
|
||||
# public visibility, and this user doesn't have permission
|
||||
# to see/work on it.
|
||||
raise PermissionDenied
|
||||
|
||||
user_has_doc_perm = not doc or doc.allows(user, "edit")
|
||||
user_has_rev_perm = not doc or doc.allows(user, "create_revision")
|
||||
|
@ -1031,7 +1055,9 @@ def translate(request, document_slug, revision_id=None):
|
|||
@login_required
|
||||
def watch_document(request, document_slug):
|
||||
"""Start watching a document for edits."""
|
||||
document = get_object_or_404(Document, locale=request.LANGUAGE_CODE, slug=document_slug)
|
||||
document = get_visible_document_or_404(
|
||||
request.user, locale=request.LANGUAGE_CODE, slug=document_slug
|
||||
)
|
||||
EditDocumentEvent.notify(request.user, document)
|
||||
return HttpResponseRedirect(document.get_absolute_url())
|
||||
|
||||
|
@ -1040,7 +1066,9 @@ def watch_document(request, document_slug):
|
|||
@login_required
|
||||
def unwatch_document(request, document_slug):
|
||||
"""Stop watching a document for edits."""
|
||||
document = get_object_or_404(Document, locale=request.LANGUAGE_CODE, slug=document_slug)
|
||||
document = get_visible_document_or_404(
|
||||
request.user, locale=request.LANGUAGE_CODE, slug=document_slug
|
||||
)
|
||||
EditDocumentEvent.stop_notifying(request.user, document)
|
||||
return HttpResponseRedirect(document.get_absolute_url())
|
||||
|
||||
|
@ -1162,6 +1190,11 @@ def helpful_vote(request, document_slug):
|
|||
return HttpResponseBadRequest()
|
||||
|
||||
revision = get_object_or_404(Revision, id=smart_int(request.POST["revision_id"]))
|
||||
|
||||
if not revision.is_approved:
|
||||
# I don't think it makes sense to vote for an unapproved revision.
|
||||
raise PermissionDenied
|
||||
|
||||
survey = None
|
||||
|
||||
if revision.document.category == TEMPLATES_CATEGORY:
|
||||
|
@ -1234,7 +1267,9 @@ def unhelpful_survey(request):
|
|||
|
||||
@require_GET
|
||||
def get_helpful_votes_async(request, document_slug):
|
||||
document = get_object_or_404(Document, locale=request.LANGUAGE_CODE, slug=document_slug)
|
||||
document = get_visible_document_or_404(
|
||||
request.user, locale=request.LANGUAGE_CODE, slug=document_slug
|
||||
)
|
||||
|
||||
datums = []
|
||||
flag_data = []
|
||||
|
@ -1334,7 +1369,12 @@ def get_helpful_votes_async(request, document_slug):
|
|||
@login_required
|
||||
def delete_revision(request, document_slug, revision_id):
|
||||
"""Delete a revision."""
|
||||
revision = get_object_or_404(Revision, pk=revision_id, document__slug=document_slug)
|
||||
revision = get_visible_revision_or_404(
|
||||
request.user,
|
||||
pk=revision_id,
|
||||
document__slug=document_slug,
|
||||
document__locale=request.LANGUAGE_CODE,
|
||||
)
|
||||
document = revision.document
|
||||
|
||||
if not document.allows(request.user, "delete_revision"):
|
||||
|
@ -1370,7 +1410,12 @@ def delete_revision(request, document_slug, revision_id):
|
|||
@require_POST
|
||||
def mark_ready_for_l10n_revision(request, document_slug, revision_id):
|
||||
"""Mark a revision as ready for l10n."""
|
||||
revision = get_object_or_404(Revision, pk=revision_id, document__slug=document_slug)
|
||||
revision = get_visible_revision_or_404(
|
||||
request.user,
|
||||
pk=revision_id,
|
||||
document__slug=document_slug,
|
||||
document__locale=settings.WIKI_DEFAULT_LANGUAGE,
|
||||
)
|
||||
|
||||
if not revision.document.allows(request.user, "mark_ready_for_l10n"):
|
||||
raise PermissionDenied
|
||||
|
@ -1392,8 +1437,10 @@ def mark_ready_for_l10n_revision(request, document_slug, revision_id):
|
|||
|
||||
@login_required
|
||||
def delete_document(request, document_slug):
|
||||
"""Delete a revision."""
|
||||
document = get_object_or_404(Document, locale=request.LANGUAGE_CODE, slug=document_slug)
|
||||
"""Delete a document."""
|
||||
document = get_visible_document_or_404(
|
||||
request.user, locale=request.LANGUAGE_CODE, slug=document_slug
|
||||
)
|
||||
|
||||
# Check permission
|
||||
if not document.allows(request.user, "delete"):
|
||||
|
@ -1420,7 +1467,9 @@ def delete_document(request, document_slug):
|
|||
@require_POST
|
||||
def add_contributor(request, document_slug):
|
||||
"""Add a contributor to a document."""
|
||||
document = get_object_or_404(Document, locale=request.LANGUAGE_CODE, slug=document_slug)
|
||||
document = get_visible_document_or_404(
|
||||
request.user, locale=request.LANGUAGE_CODE, slug=document_slug
|
||||
)
|
||||
|
||||
if not document.allows(request.user, "edit"):
|
||||
raise PermissionDenied
|
||||
|
@ -1445,7 +1494,9 @@ def add_contributor(request, document_slug):
|
|||
@require_http_methods(["GET", "POST"])
|
||||
def remove_contributor(request, document_slug, user_id):
|
||||
"""Remove a contributor from a document."""
|
||||
document = get_object_or_404(Document, locale=request.LANGUAGE_CODE, slug=document_slug)
|
||||
document = get_visible_document_or_404(
|
||||
request.user, locale=request.LANGUAGE_CODE, slug=document_slug
|
||||
)
|
||||
|
||||
if not document.allows(request.user, "edit"):
|
||||
raise PermissionDenied
|
||||
|
@ -1466,9 +1517,10 @@ def remove_contributor(request, document_slug, user_id):
|
|||
|
||||
|
||||
def show_translations(request, document_slug):
|
||||
document = get_object_or_404(
|
||||
Document, locale=settings.WIKI_DEFAULT_LANGUAGE, slug=document_slug
|
||||
document = get_visible_document_or_404(
|
||||
request.user, locale=settings.WIKI_DEFAULT_LANGUAGE, slug=document_slug
|
||||
)
|
||||
|
||||
translated_locales = []
|
||||
untranslated_locales = []
|
||||
|
||||
|
@ -1544,23 +1596,25 @@ def recent_revisions(request):
|
|||
|
||||
fragment = request.GET.pop("fragment", None)
|
||||
form = RevisionFilterForm(request.GET)
|
||||
revs = Revision.objects.order_by("-created")
|
||||
|
||||
# We are going to ignore validation errors for the most part, but
|
||||
# this is needed to call the functions that generate `cleaned_data`
|
||||
# This helps in particular when bad user names are typed in.
|
||||
form.is_valid()
|
||||
|
||||
filters = {}
|
||||
# If something has gone very wrong, `cleaned_data` won't be there.
|
||||
if hasattr(form, "cleaned_data"):
|
||||
if form.cleaned_data.get("locale"):
|
||||
revs = revs.filter(document__locale=form.cleaned_data["locale"])
|
||||
filters.update(document__locale=form.cleaned_data["locale"])
|
||||
if form.cleaned_data.get("users"):
|
||||
revs = revs.filter(creator__in=form.cleaned_data["users"])
|
||||
filters.update(creator__in=form.cleaned_data["users"])
|
||||
if form.cleaned_data.get("start"):
|
||||
revs = revs.filter(created__gte=form.cleaned_data["start"])
|
||||
filters.update(created__gte=form.cleaned_data["start"])
|
||||
if form.cleaned_data.get("end"):
|
||||
revs = revs.filter(created__lte=form.cleaned_data["end"])
|
||||
filters.update(created__lte=form.cleaned_data["end"])
|
||||
|
||||
revs = Revision.objects.visible(request.user, **filters).order_by("-created")
|
||||
|
||||
revs = paginate(request, revs)
|
||||
|
||||
|
@ -1580,7 +1634,7 @@ def recent_revisions(request):
|
|||
def what_links_here(request, document_slug):
|
||||
"""List all documents that link to a document."""
|
||||
locale = request.GET.get("locale", request.LANGUAGE_CODE)
|
||||
doc = get_object_or_404(Document, locale=locale, slug=document_slug)
|
||||
doc = get_visible_document_or_404(request.user, locale=locale, slug=document_slug)
|
||||
|
||||
links = {}
|
||||
for link_to in doc.links_to():
|
||||
|
|
Загрузка…
Ссылка в новой задаче