fixed implementation of UGC content uploader

This commit is contained in:
2026-08-03 18:57:39 -05:00
parent 39e31d3980
commit 473b3c811b
40 changed files with 4137 additions and 87 deletions
+50
View File
@@ -298,6 +298,56 @@ class ProfilePageTests(GatedTestCase):
self.alice.userprofile.refresh_from_db()
self.assertEqual(self.alice.userprofile.bio, fancy)
def _set_bio(self, text):
self.alice.userprofile.bio = text
self.alice.userprofile.save()
def test_bio_renders_markdown(self):
self._set_bio('# Hi\n\n**bold** and *italic* and `code`')
self.gate()
resp = self.client.get(reverse('profiles:user_profile', args=['Alice']))
self.assertContains(resp, '<h1>Hi</h1>', html=True)
self.assertContains(resp, '<strong>bold</strong>', html=True)
self.assertContains(resp, '<em>italic</em>', html=True)
def test_bio_raw_html_escaped(self):
self._set_bio('<script>alert(1)</script>')
self.gate()
resp = self.client.get(reverse('profiles:user_profile', args=['Alice']))
self.assertNotContains(resp, '<script>alert(1)</script>', html=True)
self.assertContains(resp, '&lt;script&gt;alert(1)&lt;/script&gt;')
def test_bio_dangerous_link_scheme_rejected(self):
self._set_bio('[x](javascript:alert(1))')
self.gate()
resp = self.client.get(reverse('profiles:user_profile', args=['Alice']))
self.assertNotContains(resp, 'href="javascript:alert(1)"')
def test_bio_gfm_features(self):
self._set_bio('| a | b |\n|---|---|\n| 1 | 2 |\n\n~~gone~~')
self.gate()
resp = self.client.get(reverse('profiles:user_profile', args=['Alice']))
self.assertContains(resp, '<table>', html=False)
self.assertContains(resp, '<s>gone</s>', html=False)
def test_bio_preview_requires_login(self):
self.gate()
resp = self.client.post(reverse('profiles:bio_preview'), {'bio': '# x'})
self.assertEqual(resp.status_code, 302)
def test_bio_preview_renders(self):
self.gate()
self.client.login(username='Alice', password='pw')
resp = self.client.post(reverse('profiles:bio_preview'), {'bio': '**hi** *there*'})
self.assertEqual(resp.status_code, 200)
self.assertJSONEqual(resp.content, {'html': '<p><strong>hi</strong> <em>there</em></p>'})
def test_bio_preview_get_rejected(self):
self.gate()
self.client.login(username='Alice', password='pw')
resp = self.client.get(reverse('profiles:bio_preview'))
self.assertEqual(resp.status_code, 405)
class FileRequestTests(MediaTestCase, GatedTestCase):
def setUp(self):
+1
View File
@@ -13,4 +13,5 @@ urlpatterns = [
path('settings/tokens/create/', views.token_create, name='token_create'),
path('settings/tokens/<int:token_id>/delete/', views.token_delete, name='token_delete'),
path('profiles/<str:username>/', views.user_profile, name='user_profile'),
path('settings/account/bio/preview/', views.bio_preview, name='bio_preview'),
]
+15 -1
View File
@@ -8,8 +8,10 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm, UserCreationForm
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from common.markdown import render_markdown
from library.models import FileIndex
from .forms import AvatarForm, BioForm, EmailForm, UsernameForm
@@ -200,4 +202,16 @@ def user_profile(request, username):
'profile_user': user,
'is_owner': is_owner,
'bio_form': bio_form,
})
'projects': user.projects.select_related('owner', 'thumbnail').prefetch_related(
'tag_links__tag__category', 'versions',
).all(),
})
@login_required
def bio_preview(request):
"""Return the markdown-rendered HTML for a bio draft (editor live preview)."""
if request.method != 'POST':
return JsonResponse({'detail': 'Method not allowed'}, status=405)
text = request.POST.get('bio', '')
return JsonResponse({'html': render_markdown(text)})