Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/blog/templates/blog/blog_detail.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{% extends 'base.html' %}
{% load static %}
{% load image_tags %}
{% load poll_tags %}

{% block title %}{{ post.title }} - Snowsune Blog{% endblock %}

Expand All @@ -20,6 +21,7 @@

{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/blog.css' %}">
<link rel="stylesheet" href="{% static 'polls/css/polls.css' %}">
{% endblock %}

{% block extra_head %}
Expand Down Expand Up @@ -100,6 +102,16 @@ <h1 class="post-title">{{ post.title }}</h1>
{{ post.content_html|safe }}
</div>

{% if post.poll %}
<section class="post-poll">
<h2>Poll</h2>
{% poll_widget post.poll %}
<p class="poll-discuss">
<a href="{{ post.poll.get_absolute_url }}">Open poll page →</a>
</p>
</section>
{% endif %}

<!-- Post Footer -->
<footer class="post-footer">
<div class="post-actions">
Expand Down
6 changes: 4 additions & 2 deletions apps/blog/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class BlogListView(ListView):
def get_queryset(self):
queryset = (
BlogPost.objects.filter(status="published")
.select_related("author")
.select_related("author", "poll")
.prefetch_related("tags")
)

Expand Down Expand Up @@ -75,7 +75,9 @@ class BlogDetailView(DetailView):
context_object_name = "post"

def get_queryset(self):
return BlogPost.objects.select_related("author").prefetch_related("tags")
return BlogPost.objects.select_related("author", "poll").prefetch_related(
"tags", "poll__choices"
)

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
Expand Down
1 change: 1 addition & 0 deletions apps/polls/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Tyro & Vixi's polls app!
118 changes: 118 additions & 0 deletions apps/polls/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from django import forms
from django.contrib import admin
from django.utils.html import format_html

from .models import Choice, Poll, Vote


class ChoiceInline(admin.TabularInline):
model = Choice
extra = 3
fields = ["text", "order"]


class PollAdminForm(forms.ModelForm):
class Meta:
model = Poll
fields = "__all__"
widgets = {
"description": forms.Textarea(
attrs={"rows": 8, "placeholder": "Markdown is fine here~"}
),
"flair": forms.TextInput(
attrs={"placeholder": "Community, Site, Comics, Events..."}
),
}


@admin.register(Poll)
class PollAdmin(admin.ModelAdmin):
form = PollAdminForm
list_display = [
"title",
"flair",
"choice_type",
"status",
"is_open_display",
"closes_at",
"vote_count",
"blog_link",
]
list_filter = ["status", "choice_type", "flair"]
search_fields = ["title", "description", "flair"]
prepopulated_fields = {"slug": ("title",)}
readonly_fields = ["created_at", "updated_at", "description_html", "blog_post"]
inlines = [ChoiceInline]
autocomplete_fields = ["created_by"]

fieldsets = (
(
"Poll",
{
"fields": (
"title",
"slug",
"flair",
"description",
"description_html",
"choice_type",
"max_choices",
)
},
),
(
"Schedule",
{"fields": ("status", "opens_at", "closes_at")},
),
(
"Discord votes",
{
"fields": ("discord_votes",),
"description": (
"Paste counts after the poll closes. Keys can be choice labels "
'or ids, e.g. {"Tacos": 12, "Sushi": 7} or {"1": 12, "2": 7}.'
),
},
),
(
"Blog",
{"fields": ("publish_to_blog", "blog_post")},
),
(
"Meta",
{
"fields": ("created_by", "created_at", "updated_at"),
"classes": ("collapse",),
},
),
)

def save_model(self, request, obj, form, change):
if not obj.created_by_id:
obj.created_by = request.user
super().save_model(request, obj, form, change)

@admin.display(boolean=True, description="Open now")
def is_open_display(self, obj):
return obj.is_open

@admin.display(description="Site votes")
def vote_count(self, obj):
return obj.votes.count()

@admin.display(description="Blog post")
def blog_link(self, obj):
if not obj.blog_post:
return "—"
url = obj.blog_post.get_absolute_url()
return format_html('<a href="{}">{}</a>', url, obj.blog_post.slug)


@admin.register(Vote)
class VoteAdmin(admin.ModelAdmin):
list_display = ["user", "poll", "choice", "created_at"]
list_filter = ["poll", "created_at"]
search_fields = ["user__username", "poll__title", "choice__text"]
autocomplete_fields = ["user", "poll"]
raw_id_fields = ["choice"]
readonly_fields = ["created_at", "updated_at"]
7 changes: 7 additions & 0 deletions apps/polls/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.apps import AppConfig


class PollsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.polls"
verbose_name = "Polls"
56 changes: 56 additions & 0 deletions apps/polls/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from django import forms

from .models import Poll


class VoteForm(forms.Form):
"""Single or multi choice vote form, built from a poll's options."""

def __init__(self, poll, *args, **kwargs):
super().__init__(*args, **kwargs)
self.poll = poll
choices = [(c.pk, c.text) for c in poll.choices.all()]

if poll.choice_type == Poll.CHOICE_SINGLE:
self.fields["choice"] = forms.ChoiceField(
choices=choices,
widget=forms.RadioSelect,
required=True,
label="Your vote",
)
else:
self.fields["choices"] = forms.MultipleChoiceField(
choices=choices,
widget=forms.CheckboxSelectMultiple,
required=True,
label="Your votes",
)

def clean(self):
cleaned = super().clean()
poll = self.poll
valid_ids = {c.pk for c in poll.choices.all()}

if poll.choice_type == Poll.CHOICE_SINGLE:
raw = cleaned.get("choice")
if not raw:
raise forms.ValidationError("Pick one option.")
choice_id = int(raw)
if choice_id not in valid_ids:
raise forms.ValidationError("That option is not on this poll.")
cleaned["choice_ids"] = [choice_id]
else:
raw_list = cleaned.get("choices") or []
if not raw_list:
raise forms.ValidationError("Pick at least one option.")
choice_ids = [int(pk) for pk in raw_list]
if any(pk not in valid_ids for pk in choice_ids):
raise forms.ValidationError("One of those options is not on this poll.")
if poll.max_choices and len(choice_ids) > poll.max_choices:
raise forms.ValidationError(
f"You can pick at most {poll.max_choices} option"
f"{'s' if poll.max_choices != 1 else ''}."
)
cleaned["choice_ids"] = choice_ids

return cleaned
Loading
Loading