Skip to content

Commit

Permalink
Fine-grained access to ban/unban users, delete/pin/mark global posts,…
Browse files Browse the repository at this point in the history
… and view/delete user comments (#327)

* Add permission to ban. Add unban button in profile page

* Add delete/pin/mark global to blog form and permissions

* Add permission to view/delete all comments by a user
  • Loading branch information
hieplpvip authored Sep 22, 2023
1 parent 1cbb7ab commit 79ce809
Show file tree
Hide file tree
Showing 13 changed files with 162 additions and 9 deletions.
2 changes: 2 additions & 0 deletions dmoj/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ def paged_list_view(view, name):
path('user/<str:user>', include([
path('', user.UserAboutPage.as_view(), name='user_page'),
path('/ban', user.UserBan.as_view(), name='user_ban'),
path('/unban', user.UserUnban.as_view(), name='user_unban'),
path('/blog/', paged_list_view(user.UserBlogPage, 'user_blog')),
path('/comment/', paged_list_view(user.UserCommentPage, 'user_comment')),
path('/solved/', include([
Expand Down Expand Up @@ -334,6 +335,7 @@ def paged_list_view(view, name):
path('post/<int:id>-<slug:slug>', include([
path('', blog.PostView.as_view(), name='blog_post'),
path('/edit', blog.BlogPostEdit.as_view(), name='blog_post_edit'),
path('/delete', blog.BlogPostDelete.as_view(), name='blog_post_delete'),
path('/', lambda _, id, slug: HttpResponsePermanentRedirect(reverse('blog_post', args=[id, slug]))),
])),

Expand Down
10 changes: 8 additions & 2 deletions judge/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ def clean(self):
return super(CustomAuthenticationForm, self).clean()

def confirm_login_allowed(self, user):
if not user.is_active and user.profile.ban_reason:
if user.profile.is_banned:
raise forms.ValidationError(
_('This account has been banned. Reason: %s') % user.profile.ban_reason,
code='banned',
Expand Down Expand Up @@ -631,11 +631,17 @@ def clean(self) -> None:
class BlogPostForm(ModelForm):
def __init__(self, *args, **kwargs):
kwargs.pop('org_pk', None)
self.user = kwargs.pop('user', None)
super(BlogPostForm, self).__init__(*args, **kwargs)

if not self.user.has_perm('judge.mark_global_post'):
self.fields.pop('global_post')
if not self.user.has_perm('judge.pin_post'):
self.fields.pop('sticky')

class Meta:
model = BlogPost
fields = ['title', 'publish_on', 'visible', 'content']
fields = ['title', 'publish_on', 'visible', 'global_post', 'sticky', 'content']
widgets = {
'content': MartorWidget(attrs={'data-markdownfy-url': reverse_lazy('blog_preview')}),
'summary': MartorWidget(attrs={'data-markdownfy-url': reverse_lazy('blog_preview')}),
Expand Down
17 changes: 17 additions & 0 deletions judge/migrations/0194_ban_permission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 3.2.21 on 2023-09-15 12:24

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('judge', '0193_unique_organization_slug'),
]

operations = [
migrations.AlterModelOptions(
name='profile',
options={'permissions': (('test_site', 'Shows in-progress development stuff'), ('totp', 'Edit TOTP settings'), ('can_upload_image', 'Can upload image directly to server via martor'), ('high_problem_timelimit', 'Can set high problem timelimit'), ('long_contest_duration', 'Can set long contest duration'), ('create_mass_testcases', 'Can create unlimitted number of testcases for a problem'), ('ban_user', 'Ban users')), 'verbose_name': 'user profile', 'verbose_name_plural': 'user profiles'},
),
]
17 changes: 17 additions & 0 deletions judge/migrations/0195_post_pin_global_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 3.2.21 on 2023-09-15 12:25

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('judge', '0194_ban_permission'),
]

operations = [
migrations.AlterModelOptions(
name='blogpost',
options={'permissions': (('edit_all_post', 'Edit all posts'), ('edit_organization_post', 'Edit organization posts'), ('mark_global_post', 'Mark post as global'), ('pin_post', 'Pin post')), 'verbose_name': 'blog post', 'verbose_name_plural': 'blog posts'},
),
]
17 changes: 17 additions & 0 deletions judge/migrations/0196_view_all_user_comment_permission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 3.2.21 on 2023-09-20 13:41

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('judge', '0195_post_pin_global_permissions'),
]

operations = [
migrations.AlterModelOptions(
name='comment',
options={'permissions': (('view_all_user_comment', 'View all comments by a user'),), 'verbose_name': 'comment', 'verbose_name_plural': 'comments'},
),
]
3 changes: 3 additions & 0 deletions judge/models/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class Comment(MPTTModel):
revisions = models.IntegerField(verbose_name=_('revisions'), default=0)

class Meta:
permissions = (
('view_all_user_comment', _('View all comments by a user')),
)
verbose_name = _('comment')
verbose_name_plural = _('comments')

Expand Down
2 changes: 2 additions & 0 deletions judge/models/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ class Meta:
permissions = (
('edit_all_post', _('Edit all posts')),
('edit_organization_post', _('Edit organization posts')),
('mark_global_post', _('Mark post as global')),
('pin_post', _('Pin post')),
)
verbose_name = _('blog post')
verbose_name_plural = _('blog posts')
Expand Down
19 changes: 19 additions & 0 deletions judge/models/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,13 @@ def has_enough_solves(self):
def is_new_user(self):
return not self.user.is_staff and not self.has_enough_solves

@cached_property
def is_banned(self):
return self.ban_reason and not self.user.is_active

def can_be_banned_by(self, staff):
return self.user != staff and not self.user.is_superuser and staff.has_perm('judge.ban_user')

@cached_property
def can_tag_problems(self):
if self.allow_tagging:
Expand Down Expand Up @@ -360,6 +367,17 @@ def ban_user(self, reason):

ban_user.alters_data = True

def unban_user(self):
self.ban_reason = ''
self.display_rank = Profile._meta.get_field('display_rank').get_default()
self.is_unlisted = False
self.save(update_fields=['ban_reason', 'display_rank', 'is_unlisted'])

self.user.is_active = True
self.user.save(update_fields=['is_active'])

unban_user.alters_data = True

def get_absolute_url(self):
return reverse('user_page', args=(self.user.username,))

Expand Down Expand Up @@ -388,6 +406,7 @@ class Meta:
('high_problem_timelimit', _('Can set high problem timelimit')),
('long_contest_duration', _('Can set long contest duration')),
('create_mass_testcases', _('Can create unlimitted number of testcases for a problem')),
('ban_user', _('Ban users')),
)
verbose_name = _('user profile')
verbose_name_plural = _('user profiles')
Expand Down
30 changes: 30 additions & 0 deletions judge/views/blog.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.utils import timezone
from django.utils.translation import gettext as _
from django.views.generic import CreateView, ListView, UpdateView
from django.views.generic.detail import SingleObjectMixin, View
from reversion import revisions

from judge.comments import CommentedDetailView
Expand Down Expand Up @@ -270,13 +271,19 @@ class BlogPostCreate(TitleMixin, CreateView):
template_name = 'blog/edit.html'
model = BlogPost
form_class = BlogPostForm
context_object_name = 'post'

def get_title(self):
return _('Creating new blog post')

def get_content_title(self):
return _('Creating new blog post')

def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs

def form_valid(self, form):
with revisions.create_revision(atomic=True):
post = form.save()
Expand Down Expand Up @@ -309,6 +316,7 @@ class BlogPostEdit(BlogPostMixin, TitleMixin, UpdateView):
template_name = 'blog/edit.html'
model = BlogPost
form_class = BlogPostForm
context_object_name = 'post'

def get_title(self):
return _('Updating blog post')
Expand All @@ -319,8 +327,14 @@ def get_content_title(self):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['edit'] = True
context['delete'] = self.request.user.has_perm('judge.delete_blogpost')
return context

def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs

def form_valid(self, form):
with revisions.create_revision(atomic=True):
revisions.set_comment(_('Edited from site'))
Expand All @@ -332,3 +346,19 @@ def dispatch(self, request, *args, **kwargs):
return generic_message(request, _('Permission denied'),
_('You cannot edit blog post.'))
return super().dispatch(request, *args, **kwargs)


class BlogPostDelete(BlogPostMixin, SingleObjectMixin, View):
def dispatch(self, request, *args, **kwargs):
if request.method != 'POST':
return HttpResponseForbidden()

return super().dispatch(request, *args, **kwargs)

def post(self, request, *args, **kwargs):
if not request.user.has_perm('judge.delete_blogpost'):
raise PermissionDenied()

post = self.get_object()
post.delete()
return HttpResponseRedirect('/')
28 changes: 24 additions & 4 deletions judge/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from django.db.models.expressions import Value
from django.db.models.fields import DateField
from django.db.models.functions import Cast, Coalesce, ExtractYear
from django.forms import Form
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
Expand Down Expand Up @@ -224,10 +225,12 @@ def get_context_data(self, **kwargs):


class UserBan(UserMixin, TitleMixin, SingleObjectFormView):
title = gettext_lazy('Ban user')
template_name = 'user/ban.html'
form_class = UserBanForm

def get_title(self):
return _('Ban {0}').format(self.object.user.username)

def form_valid(self, form):
user = self.object
with revisions.create_revision(atomic=True):
Expand All @@ -238,11 +241,28 @@ def form_valid(self, form):
return HttpResponseRedirect(reverse('user_page', args=(user.user.username,)))

def dispatch(self, request, *args, **kwargs):
if not self.request.user.is_superuser:
self.object = self.get_object()
if not self.object.can_be_banned_by(self.request.user):
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)


class UserUnban(UserBan):
form_class = Form

def get_title(self):
return _('Unban {0}').format(self.object.user.username)

def form_valid(self, form):
user = self.object
with revisions.create_revision(atomic=True):
user.unban_user()
revisions.set_user(self.request.user)
revisions.set_comment(_('Unbanned by %s') % self.request.user)

return HttpResponseRedirect(reverse('user_page', args=(user.user.username,)))


class UserBlogPage(CustomUserMixin, PostListBase):
template_name = 'user/blog.html'

Expand Down Expand Up @@ -289,7 +309,7 @@ def get_context_data(self, **kwargs):

@method_decorator(require_POST)
def delete_comments(self, request, *args, **kwargs):
if not request.user.is_superuser:
if not request.user.has_perm('judge.change_comment'):
raise PermissionDenied()

user_id = User.objects.get(username=kwargs['user']).id
Expand All @@ -300,7 +320,7 @@ def delete_comments(self, request, *args, **kwargs):
return HttpResponseRedirect(reverse('user_comment', args=(user.user.username,)))

def dispatch(self, request, *args, **kwargs):
if not self.request.user.is_superuser:
if not request.user.has_perm('judge.view_all_user_comment'):
raise PermissionDenied()
if request.method == 'POST':
return self.delete_comments(request, *args, **kwargs)
Expand Down
14 changes: 13 additions & 1 deletion templates/blog/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
time_24hr: true,
});
});

$(function () {
$('#delete-button').click(function () {
return confirm('{{ _('Are you sure you want to delete this blog post?') }}');
});
});
</script>
{{ form.media.js }}
{% endblock %}
Expand All @@ -35,11 +41,17 @@
{{ (_('Please read the [guidelines][0] before creating a new blog post.') + '\n\n [0]: /about/blog/')|markdown('blog', strip_paragraphs=True) }}
</div>
{% endif %}
{% if delete %}
<form action="{{ url('blog_post_delete', post.id, post.slug) }}" method="post" id="deleteForm">
{% csrf_token %}
</form>
{% endif %}
<form action="" method="post" class="form-area" style="display: flex; justify-content: center; flex-direction: column;">
{% csrf_token %}
<table class="django-as-table">{{ form.as_table() }}</table>
<hr>
<div style="display: flex; justify-content: flex-end;">
<div style="display: flex; justify-content: flex-end; gap: 0.5em;">
{% if delete %}<input type="submit" value="{{ _('Delete') }}" class="button" id="delete-button" form="deleteForm">{% endif %}
<input type="submit" value="{% if edit %} {{ _('Update') }} {% else %} {{ _('Create') }} {% endif %}" class="button">
</div>
</form>
Expand Down
2 changes: 2 additions & 0 deletions templates/user/comment.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@

{% block body %}
{% block before_comments %}{% endblock %}
{% if request.user.has_perm('judge.change_comment') %}
<form action="{{url('user_comment', user.username)}}" method="post">
{% csrf_token %}
<button type="submit">Delete all comments</button>
</form>
{% endif %}
<ul class="comments top-level-comments new-comments">
{% set logged_in = request.user.is_authenticated %}
{% for comment in comments %}
Expand Down
10 changes: 8 additions & 2 deletions templates/user/user-tabs.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@
{% if not request.official_contest_mode %}
{{ make_tab('blogs', 'fa-rss', url('user_blog', user.user.username), _('Blogs')) }}
{% endif %}
{% if request.user.is_superuser %}
{% if request.user.has_perm('judge.view_all_user_comment') %}
{{ make_tab('comments', 'fa-comments', url('user_comment', user.user.username), _('Comments')) }}
{% endif %}
{% if request.user.is_superuser and user.user != request.user and not user.user.is_superuser %}
{{ make_tab('impersonate', 'fa-eye', url('impersonate-start', user.user.id), _('Impersonate')) }}
{{ make_tab('ban', 'fa-ban', url('user_ban', user.user.username), _('Ban this user')) }}
{% endif %}
{% if user.can_be_banned_by(request.user) %}
{% if user.is_banned %}
{{ make_tab('unban', 'fa-life-ring', url('user_unban', user.user.username), _('Unban this user')) }}
{% else %}
{{ make_tab('ban', 'fa-ban', url('user_ban', user.user.username), _('Ban this user')) }}
{% endif %}
{% endif %}
{% if perms.auth.change_user %}
{{ make_tab('admin', 'fa-edit', url('admin:auth_user_change', user.user.id), _('Admin User')) }}
Expand Down

0 comments on commit 79ce809

Please sign in to comment.