Add organization private pages
This commit is contained in:
parent
4c3441b197
commit
4502dbf6b9
14 changed files with 369 additions and 156 deletions
|
@ -69,6 +69,7 @@ DMOJ_SUBMISSIONS_REJUDGE_LIMIT = 10
|
||||||
# Maximum number of submissions a single user can queue without the `spam_submission` permission
|
# Maximum number of submissions a single user can queue without the `spam_submission` permission
|
||||||
DMOJ_SUBMISSION_LIMIT = 3
|
DMOJ_SUBMISSION_LIMIT = 3
|
||||||
DMOJ_BLOG_NEW_PROBLEM_COUNT = 7
|
DMOJ_BLOG_NEW_PROBLEM_COUNT = 7
|
||||||
|
DMOJ_BLOG_NEW_CONTEST_COUNT = 7
|
||||||
DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7
|
DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7
|
||||||
DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1
|
DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1
|
||||||
DMOJ_USER_MAX_ORGANIZATION_COUNT = 3
|
DMOJ_USER_MAX_ORGANIZATION_COUNT = 3
|
||||||
|
|
|
@ -49,6 +49,8 @@ class BlogPostForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||||
|
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2',
|
||||||
|
attrs={'style': 'width: 100%'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
|
@ -58,7 +60,8 @@ class BlogPostForm(ModelForm):
|
||||||
|
|
||||||
class BlogPostAdmin(VersionAdmin):
|
class BlogPostAdmin(VersionAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('title', 'slug', 'authors', 'visible', 'sticky', 'publish_on')}),
|
(None, {'fields': ('title', 'slug', 'authors', 'visible', 'sticky', 'publish_on',
|
||||||
|
'is_organization_private', 'organizations')}),
|
||||||
(_('Content'), {'fields': ('content', 'og_image')}),
|
(_('Content'), {'fields': ('content', 'og_image')}),
|
||||||
(_('Summary'), {'classes': ('collapse',), 'fields': ('summary',)}),
|
(_('Summary'), {'classes': ('collapse',), 'fields': ('summary',)}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,7 +15,8 @@ class ProblemFeed(Feed):
|
||||||
description = 'The latest problems added on the %s website' % settings.SITE_LONG_NAME
|
description = 'The latest problems added on the %s website' % settings.SITE_LONG_NAME
|
||||||
|
|
||||||
def items(self):
|
def items(self):
|
||||||
return Problem.objects.filter(is_public=True, is_organization_private=False).order_by('-date', '-id')[:25]
|
return BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now(), is_organization_private=False) \
|
||||||
|
.order_by('-sticky', '-publish_on')
|
||||||
|
|
||||||
def item_title(self, problem):
|
def item_title(self, problem):
|
||||||
return problem.name
|
return problem.name
|
||||||
|
|
23
judge/migrations/0114_auto_20201228_1041.py
Normal file
23
judge/migrations/0114_auto_20201228_1041.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 2.2.17 on 2020-12-28 03:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('judge', '0113_auto_20201228_0911'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='blogpost',
|
||||||
|
name='is_organization_private',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='private to organizations'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='blogpost',
|
||||||
|
name='organizations',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='If private, only these organizations may see the blog post.', to='judge.Organization', verbose_name='organizations'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from mptt.fields import TreeForeignKey
|
from mptt.fields import TreeForeignKey
|
||||||
from mptt.models import MPTTModel
|
from mptt.models import MPTTModel
|
||||||
|
|
||||||
from judge.models.profile import Profile
|
from judge.models.profile import Organization, Profile
|
||||||
|
|
||||||
__all__ = ['MiscConfig', 'validate_regex', 'NavigationBar', 'BlogPost']
|
__all__ = ['MiscConfig', 'validate_regex', 'NavigationBar', 'BlogPost']
|
||||||
|
|
||||||
|
@ -72,6 +72,9 @@ class BlogPost(models.Model):
|
||||||
content = models.TextField(verbose_name=_('post content'))
|
content = models.TextField(verbose_name=_('post content'))
|
||||||
summary = models.TextField(verbose_name=_('post summary'), blank=True)
|
summary = models.TextField(verbose_name=_('post summary'), blank=True)
|
||||||
og_image = models.CharField(verbose_name=_('openGraph image'), default='', max_length=150, blank=True)
|
og_image = models.CharField(verbose_name=_('openGraph image'), default='', max_length=150, blank=True)
|
||||||
|
organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('organizations'),
|
||||||
|
help_text=_('If private, only these organizations may see the blog post.'))
|
||||||
|
is_organization_private = models.BooleanField(verbose_name=_('private to organizations'), default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
@ -81,11 +84,22 @@ class BlogPost(models.Model):
|
||||||
|
|
||||||
def can_see(self, user):
|
def can_see(self, user):
|
||||||
if self.visible and self.publish_on <= timezone.now():
|
if self.visible and self.publish_on <= timezone.now():
|
||||||
return True
|
if not self.is_organization_private:
|
||||||
|
return True
|
||||||
|
if user.is_authenticated and \
|
||||||
|
self.organizations.filter(id__in=user.profile.organizations.all()).exists():
|
||||||
|
return True
|
||||||
if user.has_perm('judge.edit_all_post'):
|
if user.has_perm('judge.edit_all_post'):
|
||||||
return True
|
return True
|
||||||
return user.is_authenticated and self.authors.filter(id=user.profile.id).exists()
|
return user.is_authenticated and self.authors.filter(id=user.profile.id).exists()
|
||||||
|
|
||||||
|
def is_editable_by(self, user):
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return False
|
||||||
|
if user.has_perm('judge.edit_all_post'):
|
||||||
|
return True
|
||||||
|
return user.has_perm('judge.change_blogpost') and self.authors.filter(id=user.profile.id).exists()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
('edit_all_post', _('Edit all posts')),
|
('edit_all_post', _('Edit all posts')),
|
||||||
|
|
|
@ -56,8 +56,9 @@ class BlogPostSitemap(Sitemap):
|
||||||
priority = 0.7
|
priority = 0.7
|
||||||
|
|
||||||
def items(self):
|
def items(self):
|
||||||
return BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()).values_list('id', 'slug')
|
return (BlogPost.objects.filter(visible=True, is_organization_private=False, publish_on__lte=timezone.now())
|
||||||
|
.values_list('id', 'slug'))
|
||||||
|
|
||||||
def location(self, obj):
|
def location(self, obj):
|
||||||
return reverse('blog_post', args=obj)
|
return reverse('blog_post', args=obj)
|
||||||
|
|
||||||
|
|
|
@ -30,8 +30,15 @@ class PostList(ListView):
|
||||||
orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs)
|
orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()).order_by('-sticky', '-publish_on')
|
queryset = BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()) \
|
||||||
.prefetch_related('authors__user'))
|
.order_by('-sticky', '-publish_on') \
|
||||||
|
.prefetch_related('authors__user', 'organizations')
|
||||||
|
if not self.request.user.has_perm('judge.edit_all_post'):
|
||||||
|
filter = Q(is_organization_private=False)
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
filter |= Q(organizations__in=self.request.profile.organizations.all())
|
||||||
|
queryset = queryset.filter(filter)
|
||||||
|
return queryset
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(PostList, self).get_context_data(**kwargs)
|
context = super(PostList, self).get_context_data(**kwargs)
|
||||||
|
@ -107,7 +114,7 @@ class PostView(TitleMixin, CommentedDetailView):
|
||||||
model = BlogPost
|
model = BlogPost
|
||||||
pk_url_kwarg = 'id'
|
pk_url_kwarg = 'id'
|
||||||
context_object_name = 'post'
|
context_object_name = 'post'
|
||||||
template_name = 'blog/content.html'
|
template_name = 'blog/blog.html'
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
return self.object.title
|
return self.object.title
|
||||||
|
|
|
@ -6,17 +6,18 @@ from django.core.cache import cache
|
||||||
from django.core.cache.utils import make_template_fragment_key
|
from django.core.cache.utils import make_template_fragment_key
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q, Value, BooleanField
|
||||||
from django.forms import Form, modelformset_factory
|
from django.forms import Form, modelformset_factory
|
||||||
from django.http import Http404, HttpResponsePermanentRedirect, HttpResponseRedirect
|
from django.http import Http404, HttpResponsePermanentRedirect, HttpResponseRedirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _, gettext_lazy, ungettext
|
from django.utils.translation import gettext as _, gettext_lazy, ungettext
|
||||||
from django.views.generic import DetailView, FormView, ListView, UpdateView, View
|
from django.views.generic import DetailView, FormView, ListView, UpdateView, View
|
||||||
from django.views.generic.detail import SingleObjectMixin, SingleObjectTemplateResponseMixin
|
from django.views.generic.detail import SingleObjectMixin, SingleObjectTemplateResponseMixin
|
||||||
from reversion import revisions
|
from reversion import revisions
|
||||||
|
|
||||||
from judge.forms import EditOrganizationForm
|
from judge.forms import EditOrganizationForm
|
||||||
from judge.models import Organization, OrganizationRequest, Profile
|
from judge.models import BlogPost, Comment, Organization, OrganizationRequest, Problem, Profile, Contest
|
||||||
from judge.utils.ranker import ranker
|
from judge.utils.ranker import ranker
|
||||||
from judge.utils.views import TitleMixin, generic_message
|
from judge.utils.views import TitleMixin, generic_message
|
||||||
|
|
||||||
|
@ -25,8 +26,21 @@ __all__ = ['OrganizationList', 'OrganizationHome', 'OrganizationUsers', 'Organiz
|
||||||
'OrganizationRequestDetail', 'OrganizationRequestView', 'OrganizationRequestLog',
|
'OrganizationRequestDetail', 'OrganizationRequestView', 'OrganizationRequestLog',
|
||||||
'KickUserWidgetView']
|
'KickUserWidgetView']
|
||||||
|
|
||||||
|
class OrganizationBase(object):
|
||||||
|
def can_edit_organization(self, org=None):
|
||||||
|
if org is None:
|
||||||
|
org = self.object
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
profile_id = self.request.profile.id
|
||||||
|
return org.admins.filter(id=profile_id).exists() or org.registrant_id == profile_id
|
||||||
|
|
||||||
class OrganizationMixin(object):
|
def is_member(self, org=None):
|
||||||
|
if org is None:
|
||||||
|
org = self.object
|
||||||
|
return self.request.profile in org if self.request.user.is_authenticated else False
|
||||||
|
|
||||||
|
class OrganizationMixin(OrganizationBase):
|
||||||
context_object_name = 'organization'
|
context_object_name = 'organization'
|
||||||
model = Organization
|
model = Organization
|
||||||
|
|
||||||
|
@ -46,15 +60,7 @@ class OrganizationMixin(object):
|
||||||
else:
|
else:
|
||||||
return generic_message(request, _('No such organization'),
|
return generic_message(request, _('No such organization'),
|
||||||
_('Could not find such organization.'))
|
_('Could not find such organization.'))
|
||||||
|
|
||||||
def can_edit_organization(self, org=None):
|
|
||||||
if org is None:
|
|
||||||
org = self.object
|
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
return False
|
|
||||||
profile_id = self.request.profile.id
|
|
||||||
return org.admins.filter(id=profile_id).exists() or org.registrant_id == profile_id
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationDetailView(OrganizationMixin, DetailView):
|
class OrganizationDetailView(OrganizationMixin, DetailView):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
@ -65,7 +71,7 @@ class OrganizationDetailView(OrganizationMixin, DetailView):
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationList(TitleMixin, ListView):
|
class OrganizationList(TitleMixin, ListView, OrganizationBase):
|
||||||
model = Organization
|
model = Organization
|
||||||
context_object_name = 'organizations'
|
context_object_name = 'organizations'
|
||||||
template_name = 'organization/list.html'
|
template_name = 'organization/list.html'
|
||||||
|
@ -74,6 +80,15 @@ class OrganizationList(TitleMixin, ListView):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super(OrganizationList, self).get_queryset().annotate(member_count=Count('member'))
|
return super(OrganizationList, self).get_queryset().annotate(member_count=Count('member'))
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(OrganizationList, self).get_context_data(**kwargs)
|
||||||
|
context['my_organizations'] = set()
|
||||||
|
|
||||||
|
for organization in context['organizations']:
|
||||||
|
if self.can_edit_organization(organization) or self.is_member(organization):
|
||||||
|
context['my_organizations'].add(organization)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
class OrganizationHome(OrganizationDetailView):
|
class OrganizationHome(OrganizationDetailView):
|
||||||
template_name = 'organization/home.html'
|
template_name = 'organization/home.html'
|
||||||
|
@ -82,6 +97,23 @@ class OrganizationHome(OrganizationDetailView):
|
||||||
context = super(OrganizationHome, self).get_context_data(**kwargs)
|
context = super(OrganizationHome, self).get_context_data(**kwargs)
|
||||||
context['title'] = self.object.name
|
context['title'] = self.object.name
|
||||||
context['can_edit'] = self.can_edit_organization()
|
context['can_edit'] = self.can_edit_organization()
|
||||||
|
context['is_member'] = self.is_member()
|
||||||
|
context['new_problems'] = Problem.objects.filter(is_public=True, is_organization_private=True,
|
||||||
|
organizations=self.object) \
|
||||||
|
.order_by('-date', '-id')[:settings.DMOJ_BLOG_NEW_PROBLEM_COUNT]
|
||||||
|
context['new_contests'] = Contest.objects.filter(is_visible=True, is_organization_private=True,
|
||||||
|
organizations=self.object) \
|
||||||
|
.order_by('-end_time', '-id')[:settings.DMOJ_BLOG_NEW_CONTEST_COUNT]
|
||||||
|
|
||||||
|
context['posts'] = BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now(),
|
||||||
|
is_organization_private=True, organizations=self.object) \
|
||||||
|
.order_by('-sticky', '-publish_on') \
|
||||||
|
.prefetch_related('authors__user', 'organizations')
|
||||||
|
context['post_comment_counts'] = {
|
||||||
|
int(page[2:]): count for page, count in
|
||||||
|
Comment.objects.filter(page__in=['b:%d' % post.id for post in context['posts']], hidden=False)
|
||||||
|
.values_list('page').annotate(count=Count('page')).order_by()
|
||||||
|
}
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -76,6 +76,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blog-comment-count {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-comment-icon {
|
||||||
|
padding: 0.1em 0.2em 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-comment-count-link {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
.blog-content, .blog-sidebar {
|
.blog-content, .blog-sidebar {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
|
52
templates/blog/blog.html
Normal file
52
templates/blog/blog.html
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block js_media %}
|
||||||
|
{% include "comments/media-js.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block media %}
|
||||||
|
{% include "comments/media-css.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
{% if post.is_editable_by(request.user) %}
|
||||||
|
<div class="title-line-action">[<a href="{{ url('admin:judge_blogpost_change', post.id) }}">{{ _('Edit') }}</a>]</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="post-full">
|
||||||
|
<div class="time">
|
||||||
|
{% with authors=post.authors.all() %}
|
||||||
|
{% if authors %}
|
||||||
|
<span class="post-authors">{{ link_users(authors) }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
<span class="post-time">
|
||||||
|
{% trans time=post.publish_on|date(_("N j, Y, g:i a")) %}
|
||||||
|
posted on {{ time }}
|
||||||
|
{% endtrans %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="body content-description">
|
||||||
|
{% cache 86400 'post_content' post.id MATH_ENGINE %}
|
||||||
|
{{ post.content|markdown('blog', MATH_ENGINE)|reference|str|safe}}
|
||||||
|
{% endcache %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<span class="social">
|
||||||
|
{{ post_to_gplus(request, post, '<i class="fa fa-google-plus-square"></i>') }}
|
||||||
|
{{ post_to_facebook(request, post, '<i class="fa fa-facebook-official"></i>') }}
|
||||||
|
{{ post_to_twitter(request, SITE_NAME + ':', post, '<i class="fa fa-twitter"></i>') }}
|
||||||
|
</span>
|
||||||
|
{% include "comments/list.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block bodyend %}
|
||||||
|
{{ super() }}
|
||||||
|
{% if REQUIRE_JAX %}
|
||||||
|
{% include "mathjax-load.html" %}
|
||||||
|
{% endif %}
|
||||||
|
{% include "comments/math.html" %}
|
||||||
|
{% endblock %}
|
|
@ -1,53 +1,40 @@
|
||||||
{% extends "base.html" %}
|
<section class="{% if post.sticky %}sticky {% endif %}post">
|
||||||
|
<span style="float:right">
|
||||||
{% block js_media %}
|
<span class="time">
|
||||||
{% include "comments/media-js.html" %}
|
{%- if post.sticky %}<i title="Sticky" class="fa fa-star fa-fw"></i>{% endif -%}
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block media %}
|
|
||||||
{% include "comments/media-css.html" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
{% if perms.judge.change_blogpost %}
|
|
||||||
<div class="title-line-action">[<a href="{{ url('admin:judge_blogpost_change', post.id) }}">{{ _('Edit') }}</a>]
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<div class="post-full">
|
|
||||||
<div class="time">
|
|
||||||
{% with authors=post.authors.all() %}
|
{% with authors=post.authors.all() %}
|
||||||
{% if authors %}
|
{%- if authors -%}
|
||||||
<span class="post-authors">{{ link_users(authors) }}</span>
|
<span class="post-authors">{{ link_users(authors) }}</span>
|
||||||
{% endif %}
|
{%- endif -%}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<span class="post-time">
|
{{ relative_time(post.publish_on, abs=_('posted on {time}'), rel=_('posted {time}')) -}}
|
||||||
{% trans time=post.publish_on|date(_("N j, Y, g:i a")) %}
|
</span>
|
||||||
posted on {{ time }}
|
<span>
|
||||||
{% endtrans %}
|
<a href="{{ url('blog_post', post.id, post.slug) }}#comments" class="blog-comment-count-link">
|
||||||
</span>
|
<i class="fa fa-comments blog-comment-icon"></i>
|
||||||
</div>
|
<span class="blog-comment-count">
|
||||||
<div class="body content-description">
|
{{- post_comment_counts[post.id] or 0 -}}
|
||||||
{% cache 86400 'post_content' post.id MATH_ENGINE %}
|
</span>
|
||||||
{{ post.content|markdown('blog', MATH_ENGINE)|reference|str|safe}}
|
</a>
|
||||||
{% endcache %}
|
</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<span class="social">
|
|
||||||
{{ post_to_gplus(request, post, '<i class="fa fa-google-plus-square"></i>') }}
|
|
||||||
{{ post_to_facebook(request, post, '<i class="fa fa-facebook-official"></i>') }}
|
|
||||||
{{ post_to_twitter(request, SITE_NAME + ':', post, '<i class="fa fa-twitter"></i>') }}
|
|
||||||
</span>
|
</span>
|
||||||
{% include "comments/list.html" %}
|
<h2 class="title">
|
||||||
{% endblock %}
|
<a href="{{ url('blog_post', post.id, post.slug) }}">{{ post.title }}</a>
|
||||||
|
</h2>
|
||||||
{% block bodyend %}
|
{% if post.is_organization_private and show_organization_private_icon %}
|
||||||
{{ super() }}
|
<div class="organization-tags">
|
||||||
{% if REQUIRE_JAX %}
|
{% for org in post.organizations.all() %}
|
||||||
{% include "mathjax-load.html" %}
|
<span class="organization-tag">
|
||||||
|
<a href="{{ org.get_absolute_url() }}">
|
||||||
|
<i class="fa fa-lock"></i> {{ org.name }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "comments/math.html" %}
|
<div class="summary content-description">
|
||||||
{% endblock %}
|
{% cache 86400 'post_summary' post.id %}
|
||||||
|
{{ post.summary|default(post.content, true)|markdown('blog', 'svg', lazy_load=True)|reference|str|safe }}
|
||||||
|
{% endcache %}
|
||||||
|
</div>
|
||||||
|
</section>
|
|
@ -27,18 +27,6 @@
|
||||||
margin-top: 0.6em;
|
margin-top: 0.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-count {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-icon {
|
|
||||||
padding: 0.1em 0.2em 0 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-count-link {
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.own-open-tickets .title a, .open-tickets .title a {
|
.own-open-tickets .title a, .open-tickets .title a {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
@ -103,35 +91,9 @@
|
||||||
<div class="blog-content sidebox">
|
<div class="blog-content sidebox">
|
||||||
<h3>{{ _('News') }} <i class="fa fa-terminal"></i></h3>
|
<h3>{{ _('News') }} <i class="fa fa-terminal"></i></h3>
|
||||||
<div class="sidebox-content">
|
<div class="sidebox-content">
|
||||||
|
{% set show_organization_private_icon=True %}
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
<section class="{% if post.sticky %}sticky {% endif %}post">
|
{% include "blog/content.html" %}
|
||||||
<span style="float:right;">
|
|
||||||
<span class="time">
|
|
||||||
{%- if post.sticky %}<i title="Sticky" class="fa fa-star fa-fw"></i>{% endif -%}
|
|
||||||
{% with authors=post.authors.all() %}
|
|
||||||
{%- if authors -%}
|
|
||||||
<span class="post-authors">{{ link_users(authors) }}</span>
|
|
||||||
{%- endif -%}
|
|
||||||
{% endwith %}
|
|
||||||
{{ relative_time(post.publish_on, abs=_('posted on {time}'), rel=_('posted {time}')) -}}
|
|
||||||
</span><span class="comment-data">
|
|
||||||
<a href="{{ url('blog_post', post.id, post.slug) }}#comments" class="comment-count-link">
|
|
||||||
<i class="fa fa-comments comment-icon"></i><span class="comment-count">
|
|
||||||
{{- post_comment_counts[post.id] or 0 -}}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<h2 class="title">
|
|
||||||
<a href="{{ url('blog_post', post.id, post.slug) }}">{{ post.title }}</a>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="summary content-description">
|
|
||||||
{% cache 86400 'post_summary' post.id %}
|
|
||||||
{{ post.summary|default(post.content, true)|markdown('blog', 'svg', lazy_load=True)|reference|str|safe }}
|
|
||||||
{% endcache %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% if page_obj.num_pages > 1 %}
|
{% if page_obj.num_pages > 1 %}
|
||||||
|
|
|
@ -1,6 +1,19 @@
|
||||||
{% extends "common-content.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% block title_row %}{% endblock %}
|
||||||
|
{% block title_ruler %}{% endblock %}
|
||||||
|
|
||||||
{% block content_js_media %}
|
<!-- {% block media %}
|
||||||
|
<style>
|
||||||
|
.post {
|
||||||
|
margin: 0 1.4em;
|
||||||
|
}
|
||||||
|
.post:first-child {
|
||||||
|
margin-top: 0.6em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %} -->
|
||||||
|
|
||||||
|
{% block js_media %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function () {
|
$(function () {
|
||||||
$('.leave-organization').click(function () {
|
$('.leave-organization').click(function () {
|
||||||
|
@ -12,52 +25,133 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('.blog-sidebar').hide();
|
||||||
|
$('#info-tab').find('a').click(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$('#info-tab').addClass('active');
|
||||||
|
$('#detail-tab').removeClass('active');
|
||||||
|
$('.blog-content').show();
|
||||||
|
$('.blog-sidebar').hide();
|
||||||
|
});
|
||||||
|
$('#detail-tab').find('a').click(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$('#detail-tab').addClass('active');
|
||||||
|
$('#info-tab').removeClass('active');
|
||||||
|
$('.blog-content').hide();
|
||||||
|
$('.blog-sidebar').show();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block info_float %}
|
{% block body %}
|
||||||
{% if request.user.is_authenticated %}
|
{% block before_posts %}{% endblock %}
|
||||||
{% if request.profile in organization %}
|
<div id="mobile" class="tabs">
|
||||||
<form method="post" action="{{ url('leave_organization', organization.id, organization.slug) }}">
|
<ul>
|
||||||
{% csrf_token %}
|
<li id="info-tab" class="tab active"><a href="#">
|
||||||
<input type="submit" class="unselectable button full leave-organization" value="{{ _('Leave organization') }}">
|
<i class="tab-icon fa fa-info-circle"></i> {{ _('Info') }}
|
||||||
</form>
|
</a></li>
|
||||||
{% elif organization.is_open %}
|
<li id="detail-tab" class="tab"><a href="#"><i class="tab-icon fa fa-rss"></i> {{ _('Details') }}</a></li>
|
||||||
<form method="post" action="{{ url('join_organization', organization.id, organization.slug) }}">
|
</ul>
|
||||||
{% csrf_token %}
|
|
||||||
<input type="submit" class="unselectable button full" value="{{ _('Join organization') }}">
|
|
||||||
</form>
|
|
||||||
{% else %}
|
|
||||||
<a href="{{ url('request_organization', organization.id, organization.slug) }}"
|
|
||||||
class="unselectable button full">{{ _('Request membership') }}</a>
|
|
||||||
{% endif %}
|
|
||||||
<hr>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if can_edit %}
|
|
||||||
<div><a href="{{ url('edit_organization', organization.id, organization.slug) }}">{{ _('Edit organization') }}</a></div>
|
|
||||||
|
|
||||||
{% if not organization.is_open %}
|
|
||||||
<div>
|
|
||||||
<a href="{{ url('organization_requests_pending', organization.id, organization.slug) }}">{{ _('View requests') }}</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if perms.judge.change_organization %}
|
|
||||||
<div>
|
|
||||||
<a href="{{ url('admin:judge_organization_change', organization.id) }}">{{ _('Admin organization') }}</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<a href="{{ organization.get_users_url() }}">{{ _('View members') }}</a>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
<div id="blog-container">
|
||||||
|
<div class="blog-content sidebox">
|
||||||
|
<h3>{{ _('About') }} {{ organization.name }} <i class="fa fa-info-circle"></i></h3>
|
||||||
|
<div class="sidebox-content">
|
||||||
|
<div style="margin: 1.4em;">
|
||||||
|
{% cache 3600 'organization_html' organization.id MATH_ENGINE %}
|
||||||
|
{{ organization.about|markdown('organization-about', MATH_ENGINE)|reference|str|safe }}
|
||||||
|
{% endcache %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if is_member or can_edit %}
|
||||||
|
<br>
|
||||||
|
<h3>{{ _('Organization news') }} <i class="fa fa-terminal"></i></h3>
|
||||||
|
<div class="sidebox-content">
|
||||||
|
{% for post in posts %}
|
||||||
|
{% include "blog/content.html" %}
|
||||||
|
{% else %}
|
||||||
|
<div style="margin: 1.4em;">
|
||||||
|
<i>{{ _('There is no news at this time.') }}</i>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% block description %}
|
<div class="blog-sidebar">
|
||||||
{% cache 3600 'organization_html' organization.id MATH_ENGINE %}
|
<div class="blog-sidebox sidebox">
|
||||||
{{ organization.about|markdown('organization-about', MATH_ENGINE)|reference|str|safe }}
|
<h3>{{ _('Controls') }} <i class="fa fa-cog"></i></h3>
|
||||||
{% endcache %}
|
<div class="sidebox-content" style="padding: 1em;">
|
||||||
{% endblock %}
|
{% if request.user.is_authenticated %}
|
||||||
|
{% if is_member %}
|
||||||
|
<form method="post" action="{{ url('leave_organization', organization.id, organization.slug) }}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="submit" class="unselectable button full leave-organization" value="{{ _('Leave organization') }}">
|
||||||
|
</form>
|
||||||
|
{% elif organization.is_open or can_edit %}
|
||||||
|
<form method="post" action="{{ url('join_organization', organization.id, organization.slug) }}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="submit" class="unselectable button full" value="{{ _('Join organization') }}">
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url('request_organization', organization.id, organization.slug) }}"
|
||||||
|
class="unselectable button full">{{ _('Request membership') }}</a>
|
||||||
|
{% endif %}
|
||||||
|
<br>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_edit %}
|
||||||
|
<div>
|
||||||
|
<a href="{{ url('edit_organization', organization.id, organization.slug) }}">{{ _('Edit organization') }}</a>
|
||||||
|
</div>
|
||||||
|
{% if not organization.is_open %}
|
||||||
|
<div>
|
||||||
|
<a href="{{ url('organization_requests_pending', organization.id, organization.slug) }}">{{ _('View requests') }}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.judge.change_organization %}
|
||||||
|
<div>
|
||||||
|
<a href="{{ url('admin:judge_organization_change', organization.id) }}">{{ _('Admin organization') }}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
<a href="{{ organization.get_users_url() }}">{{ _('View members') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if (is_member or can_edit) %}
|
||||||
|
{% if new_contests %}
|
||||||
|
<div class="blog-sidebox sidebox">
|
||||||
|
<h3>{{ _('New private contests') }} <i class="fa fa-trophy"></i>
|
||||||
|
</h3>
|
||||||
|
<div class="sidebox-content">
|
||||||
|
<ul class="problem-list">
|
||||||
|
{% for contest in new_contests %}
|
||||||
|
<li><a href="{{ url('contest_view', contest.key) }}">{{ contest.name }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if new_problems %}
|
||||||
|
<div class="blog-sidebox sidebox">
|
||||||
|
<h3>{{ _('New private problems') }} <i class="fa fa-puzzle-piece"></i>
|
||||||
|
</h3>
|
||||||
|
<div class="sidebox-content">
|
||||||
|
<ul class="problem-list">
|
||||||
|
{% for problem in new_problems %}
|
||||||
|
<li><a href="{{ url('problem_detail', problem.code) }}">{{ problem.name }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% block after_posts %}{% endblock %}
|
||||||
|
{% endblock %}
|
|
@ -4,8 +4,25 @@
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function () {
|
$(function () {
|
||||||
$("#organization-table").tablesorter();
|
$("#organization-table").tablesorter();
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
$('#show-my-org-checkbox').click(function() {
|
||||||
|
let checked = $('#show-my-org-checkbox').is(':checked');
|
||||||
|
if (checked) {
|
||||||
|
$('.other-organization').hide();
|
||||||
|
$('.my-organization').last().find('td').css({'border-bottom-width':
|
||||||
|
'1px', 'border-color': '#ccc'});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$('.other-organization').show();
|
||||||
|
$('.my-organization').last().find('td').css({'border-bottom-width':
|
||||||
|
'', 'border-color': ''});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$('#show-my-org-checkbox').click()
|
||||||
|
{% endif %}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title_ruler %}{% endblock %}
|
{% block title_ruler %}{% endblock %}
|
||||||
|
@ -17,6 +34,13 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<div style="margin-bottom: 0.5em">
|
||||||
|
<input id="show-my-org-checkbox" type="checkbox" style="vertical-align: bottom;">
|
||||||
|
<label for="show-my-org-checkbox" style="vertical-align: bottom; margin-right: 1em;">{{ _('Show my organizations only') }}</label>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<table id="organization-table" class="table">
|
<table id="organization-table" class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -26,7 +50,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for org in organizations %}
|
{% for org in organizations %}
|
||||||
<tr>
|
<tr class="{{ 'my-organization' if org in my_organizations else 'other-organization'}}">
|
||||||
<td><a href="{{ org.get_absolute_url() }}">{{ org.name }}</a></td>
|
<td><a href="{{ org.get_absolute_url() }}">{{ org.name }}</a></td>
|
||||||
<td><a href="{{ org.get_users_url() }}">{{ org.member_count }}</a></td>
|
<td><a href="{{ org.get_users_url() }}">{{ org.member_count }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
Loading…
Reference in a new issue