diff --git a/dmoj/settings.py b/dmoj/settings.py
index a96cf3f..4967ee7 100644
--- a/dmoj/settings.py
+++ b/dmoj/settings.py
@@ -69,6 +69,7 @@ DMOJ_SUBMISSIONS_REJUDGE_LIMIT = 10
# Maximum number of submissions a single user can queue without the `spam_submission` permission
DMOJ_SUBMISSION_LIMIT = 3
DMOJ_BLOG_NEW_PROBLEM_COUNT = 7
+DMOJ_BLOG_NEW_CONTEST_COUNT = 7
DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7
DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1
DMOJ_USER_MAX_ORGANIZATION_COUNT = 3
diff --git a/judge/admin/interface.py b/judge/admin/interface.py
index 0bfec14..4691c8b 100644
--- a/judge/admin/interface.py
+++ b/judge/admin/interface.py
@@ -49,6 +49,8 @@ class BlogPostForm(ModelForm):
class Meta:
widgets = {
'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:
@@ -58,7 +60,8 @@ class BlogPostForm(ModelForm):
class BlogPostAdmin(VersionAdmin):
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')}),
(_('Summary'), {'classes': ('collapse',), 'fields': ('summary',)}),
)
diff --git a/judge/feed.py b/judge/feed.py
index 47e9d51..f0fa81c 100644
--- a/judge/feed.py
+++ b/judge/feed.py
@@ -15,7 +15,8 @@ class ProblemFeed(Feed):
description = 'The latest problems added on the %s website' % settings.SITE_LONG_NAME
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):
return problem.name
diff --git a/judge/migrations/0114_auto_20201228_1041.py b/judge/migrations/0114_auto_20201228_1041.py
new file mode 100644
index 0000000..3103b3c
--- /dev/null
+++ b/judge/migrations/0114_auto_20201228_1041.py
@@ -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'),
+ ),
+ ]
diff --git a/judge/models/interface.py b/judge/models/interface.py
index ed25d63..6d94d52 100644
--- a/judge/models/interface.py
+++ b/judge/models/interface.py
@@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
from mptt.fields import TreeForeignKey
from mptt.models import MPTTModel
-from judge.models.profile import Profile
+from judge.models.profile import Organization, Profile
__all__ = ['MiscConfig', 'validate_regex', 'NavigationBar', 'BlogPost']
@@ -72,6 +72,9 @@ class BlogPost(models.Model):
content = models.TextField(verbose_name=_('post content'))
summary = models.TextField(verbose_name=_('post summary'), 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):
return self.title
@@ -81,11 +84,22 @@ class BlogPost(models.Model):
def can_see(self, user):
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'):
return True
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:
permissions = (
('edit_all_post', _('Edit all posts')),
diff --git a/judge/sitemap.py b/judge/sitemap.py
index 5678d76..6ae8657 100644
--- a/judge/sitemap.py
+++ b/judge/sitemap.py
@@ -56,8 +56,9 @@ class BlogPostSitemap(Sitemap):
priority = 0.7
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):
return reverse('blog_post', args=obj)
diff --git a/judge/views/blog.py b/judge/views/blog.py
index 5f90c91..4244107 100644
--- a/judge/views/blog.py
+++ b/judge/views/blog.py
@@ -30,8 +30,15 @@ class PostList(ListView):
orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs)
def get_queryset(self):
- return (BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()).order_by('-sticky', '-publish_on')
- .prefetch_related('authors__user'))
+ queryset = BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()) \
+ .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):
context = super(PostList, self).get_context_data(**kwargs)
@@ -107,7 +114,7 @@ class PostView(TitleMixin, CommentedDetailView):
model = BlogPost
pk_url_kwarg = 'id'
context_object_name = 'post'
- template_name = 'blog/content.html'
+ template_name = 'blog/blog.html'
def get_title(self):
return self.object.title
diff --git a/judge/views/organization.py b/judge/views/organization.py
index a9c1053..e9b9637 100644
--- a/judge/views/organization.py
+++ b/judge/views/organization.py
@@ -6,17 +6,18 @@ from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.core.exceptions import PermissionDenied
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.http import Http404, HttpResponsePermanentRedirect, HttpResponseRedirect
from django.urls import reverse
+from django.utils import timezone
from django.utils.translation import gettext as _, gettext_lazy, ungettext
from django.views.generic import DetailView, FormView, ListView, UpdateView, View
from django.views.generic.detail import SingleObjectMixin, SingleObjectTemplateResponseMixin
from reversion import revisions
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.views import TitleMixin, generic_message
@@ -25,8 +26,21 @@ __all__ = ['OrganizationList', 'OrganizationHome', 'OrganizationUsers', 'Organiz
'OrganizationRequestDetail', 'OrganizationRequestView', 'OrganizationRequestLog',
'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'
model = Organization
@@ -46,15 +60,7 @@ class OrganizationMixin(object):
else:
return generic_message(request, _('No 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):
def get(self, request, *args, **kwargs):
@@ -65,7 +71,7 @@ class OrganizationDetailView(OrganizationMixin, DetailView):
return self.render_to_response(context)
-class OrganizationList(TitleMixin, ListView):
+class OrganizationList(TitleMixin, ListView, OrganizationBase):
model = Organization
context_object_name = 'organizations'
template_name = 'organization/list.html'
@@ -74,6 +80,15 @@ class OrganizationList(TitleMixin, ListView):
def get_queryset(self):
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):
template_name = 'organization/home.html'
@@ -82,6 +97,23 @@ class OrganizationHome(OrganizationDetailView):
context = super(OrganizationHome, self).get_context_data(**kwargs)
context['title'] = self.object.name
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
diff --git a/resources/blog.scss b/resources/blog.scss
index 55eda69..cd6aeb2 100644
--- a/resources/blog.scss
+++ b/resources/blog.scss
@@ -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) {
.blog-content, .blog-sidebar {
display: block !important;
diff --git a/templates/blog/blog.html b/templates/blog/blog.html
new file mode 100644
index 0000000..640913d
--- /dev/null
+++ b/templates/blog/blog.html
@@ -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) %}
+
+ {% endif %}
+{% endblock %}
+
+{% block body %}
+
+
+ {% with authors=post.authors.all() %}
+ {% if authors %}
+ {{ link_users(authors) }}
+ {% endif %}
+ {% endwith %}
+
+ {% trans time=post.publish_on|date(_("N j, Y, g:i a")) %}
+ posted on {{ time }}
+ {% endtrans %}
+
+
+
+ {% cache 86400 'post_content' post.id MATH_ENGINE %}
+ {{ post.content|markdown('blog', MATH_ENGINE)|reference|str|safe}}
+ {% endcache %}
+
+
+
+
+ {{ post_to_gplus(request, post, '') }}
+ {{ post_to_facebook(request, post, '') }}
+ {{ post_to_twitter(request, SITE_NAME + ':', post, '') }}
+
+ {% include "comments/list.html" %}
+{% endblock %}
+
+{% block bodyend %}
+ {{ super() }}
+ {% if REQUIRE_JAX %}
+ {% include "mathjax-load.html" %}
+ {% endif %}
+ {% include "comments/math.html" %}
+{% endblock %}
\ No newline at end of file
diff --git a/templates/blog/content.html b/templates/blog/content.html
index 1ce27f8..2c0776b 100644
--- a/templates/blog/content.html
+++ b/templates/blog/content.html
@@ -1,53 +1,40 @@
-{% extends "base.html" %}
-
-{% block js_media %}
- {% include "comments/media-js.html" %}
-{% endblock %}
-
-{% block media %}
- {% include "comments/media-css.html" %}
-{% endblock %}
-
-{% block header %}
- {% if perms.judge.change_blogpost %}
-
- {% endif %}
-{% endblock %}
-
-{% block body %}
-
-
+
+
+
+ {%- if post.sticky %}{% endif -%}
{% with authors=post.authors.all() %}
- {% if authors %}
+ {%- if authors -%}
{{ link_users(authors) }}
- {% endif %}
+ {%- endif -%}
{% endwith %}
-
- {% trans time=post.publish_on|date(_("N j, Y, g:i a")) %}
- posted on {{ time }}
- {% endtrans %}
-
-
-
- {% cache 86400 'post_content' post.id MATH_ENGINE %}
- {{ post.content|markdown('blog', MATH_ENGINE)|reference|str|safe}}
- {% endcache %}
-
-
-
-
- {{ post_to_gplus(request, post, '') }}
- {{ post_to_facebook(request, post, '') }}
- {{ post_to_twitter(request, SITE_NAME + ':', post, '') }}
+ {{ relative_time(post.publish_on, abs=_('posted on {time}'), rel=_('posted {time}')) -}}
+
+
+
+
- {% include "comments/list.html" %}
-{% endblock %}
-
-{% block bodyend %}
- {{ super() }}
- {% if REQUIRE_JAX %}
- {% include "mathjax-load.html" %}
+
+ {% if post.is_organization_private and show_organization_private_icon %}
+
{% endif %}
- {% include "comments/math.html" %}
-{% endblock %}
+
+ {% cache 86400 'post_summary' post.id %}
+ {{ post.summary|default(post.content, true)|markdown('blog', 'svg', lazy_load=True)|reference|str|safe }}
+ {% endcache %}
+
+
\ No newline at end of file
diff --git a/templates/blog/list.html b/templates/blog/list.html
index ed3c62c..cf11c72 100644
--- a/templates/blog/list.html
+++ b/templates/blog/list.html
@@ -27,18 +27,6 @@
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 {
display: block;
}
@@ -103,35 +91,9 @@
{{ _('News') }}
+ {% set show_organization_private_icon=True %}
{% for post in posts %}
-
-
-
- {%- if post.sticky %}{% endif -%}
- {% with authors=post.authors.all() %}
- {%- if authors -%}
- {{ link_users(authors) }}
- {%- endif -%}
- {% endwith %}
- {{ relative_time(post.publish_on, abs=_('posted on {time}'), rel=_('posted {time}')) -}}
-
-
-
-
-
- {% cache 86400 'post_summary' post.id %}
- {{ post.summary|default(post.content, true)|markdown('blog', 'svg', lazy_load=True)|reference|str|safe }}
- {% endcache %}
-
-
+ {% include "blog/content.html" %}
{% endfor %}
{% if page_obj.num_pages > 1 %}
diff --git a/templates/organization/home.html b/templates/organization/home.html
index 3df7ac7..40839fa 100644
--- a/templates/organization/home.html
+++ b/templates/organization/home.html
@@ -1,6 +1,19 @@
-{% extends "common-content.html" %}
+{% extends "base.html" %}
+{% block title_row %}{% endblock %}
+{% block title_ruler %}{% endblock %}
-{% block content_js_media %}
+
+
+{% block js_media %}
{% endblock %}
-{% block info_float %}
- {% if request.user.is_authenticated %}
- {% if request.profile in organization %}
-
- {% elif organization.is_open %}
-
- {% else %}
-
{{ _('Request membership') }}
- {% endif %}
-
- {% endif %}
-
- {% if can_edit %}
-
-
- {% if not organization.is_open %}
-
- {% endif %}
- {% endif %}
-
- {% if perms.judge.change_organization %}
-
- {% endif %}
-
-
-
{{ _('View members') }}
+{% block body %}
+ {% block before_posts %}{% endblock %}
+
-{% endblock %}
+
+
+
{{ _('About') }} {{ organization.name }}
+
+
+ {% cache 3600 'organization_html' organization.id MATH_ENGINE %}
+ {{ organization.about|markdown('organization-about', MATH_ENGINE)|reference|str|safe }}
+ {% endcache %}
+
+
+ {% if is_member or can_edit %}
+
+
{{ _('Organization news') }}
+
+ {% for post in posts %}
+ {% include "blog/content.html" %}
+ {% else %}
+
+ {{ _('There is no news at this time.') }}
+
+ {% endfor %}
+
+ {% endif %}
+
-{% block description %}
- {% cache 3600 'organization_html' organization.id MATH_ENGINE %}
- {{ organization.about|markdown('organization-about', MATH_ENGINE)|reference|str|safe }}
- {% endcache %}
-{% endblock %}
+
+
+ {% block after_posts %}{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/templates/organization/list.html b/templates/organization/list.html
index 402fdaf..364f821 100644
--- a/templates/organization/list.html
+++ b/templates/organization/list.html
@@ -4,8 +4,25 @@
+
{% endblock %}
{% block title_ruler %}{% endblock %}
@@ -17,6 +34,13 @@
{% endblock %}
{% block body %}
+ {% if request.user.is_authenticated %}
+
+
+
+
+ {% endif %}
+