Merge pull request #31 from LQDJudge/pagevote

add bookmark pages
This commit is contained in:
Phuoc Dinh Le 2022-11-17 15:50:25 -06:00 committed by GitHub
commit bba7a761ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 354 additions and 23 deletions

View file

@ -60,6 +60,7 @@ from judge.views import (
user, user,
volunteer, volunteer,
pagevote, pagevote,
bookmark,
widgets, widgets,
internal, internal,
) )
@ -403,6 +404,7 @@ urlpatterns = [
), ),
url(r"^user$", user.UserAboutPage.as_view(), name="user_page"), url(r"^user$", user.UserAboutPage.as_view(), name="user_page"),
url(r"^edit/profile/$", user.edit_profile, name="user_edit_profile"), url(r"^edit/profile/$", user.edit_profile, name="user_edit_profile"),
url(r"^user/bookmarks", user.UserBookMarkPage.as_view(), name="user_bookmark"),
url( url(
r"^user/(?P<user>\w+)", r"^user/(?P<user>\w+)",
include( include(
@ -448,6 +450,8 @@ urlpatterns = [
), ),
url(r"^pagevotes/upvote/$", pagevote.upvote_page, name="pagevote_upvote"), url(r"^pagevotes/upvote/$", pagevote.upvote_page, name="pagevote_upvote"),
url(r"^pagevotes/downvote/$", pagevote.downvote_page, name="pagevote_downvote"), url(r"^pagevotes/downvote/$", pagevote.downvote_page, name="pagevote_downvote"),
url(r"^bookmarks/dobookmark/$", bookmark.dobookmark_page, name="dobookmark"),
url(r"^bookmarks/undobookmark/$", bookmark.undobookmark_page, name="undobookmark"),
url(r"^comments/upvote/$", comment.upvote_comment, name="comment_upvote"), url(r"^comments/upvote/$", comment.upvote_comment, name="comment_upvote"),
url(r"^comments/downvote/$", comment.downvote_comment, name="comment_downvote"), url(r"^comments/downvote/$", comment.downvote_comment, name="comment_downvote"),
url(r"^comments/hide/$", comment.comment_hide, name="comment_hide"), url(r"^comments/hide/$", comment.comment_hide, name="comment_hide"),

View file

@ -0,0 +1,38 @@
# Generated by Django 3.2.16 on 2022-11-17 17:13
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('judge', '0137_auto_20221116_2201'),
]
operations = [
migrations.CreateModel(
name='BookMark',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('page', models.CharField(db_index=True, max_length=30, verbose_name='associated page')),
],
options={
'verbose_name': 'bookmark',
'verbose_name_plural': 'bookmarks',
},
),
migrations.CreateModel(
name='MakeBookMark',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('bookmark', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bookmark', to='judge.bookmark')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_bookmark', to='judge.profile')),
],
options={
'verbose_name': 'make bookmark',
'verbose_name_plural': 'make bookmarks',
'unique_together': {('user', 'bookmark')},
},
),
]

View file

@ -55,6 +55,7 @@ from judge.models.submission import (
from judge.models.ticket import Ticket, TicketMessage from judge.models.ticket import Ticket, TicketMessage
from judge.models.volunteer import VolunteerProblemVote from judge.models.volunteer import VolunteerProblemVote
from judge.models.pagevote import PageVote, PageVoteVoter from judge.models.pagevote import PageVote, PageVoteVoter
from judge.models.bookmark import BookMark, MakeBookMark
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"]) revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
revisions.register(Problem, follow=["language_limits"]) revisions.register(Problem, follow=["language_limits"])
@ -79,4 +80,5 @@ revisions.register(ContestParticipation)
revisions.register(Rating) revisions.register(Rating)
revisions.register(PageVoteVoter) revisions.register(PageVoteVoter)
revisions.register(VolunteerProblemVote) revisions.register(VolunteerProblemVote)
revisions.register(MakeBookMark)
del revisions del revisions

57
judge/models/bookmark.py Normal file
View file

@ -0,0 +1,57 @@
from django.db import models
from django.db.models import CASCADE
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist
from judge.models import Profile
from judge.models.contest import Contest
from judge.models.interface import BlogPost
from judge.models.problem import Problem, Solution
__all__ = ["BookMark"]
class BookMark(models.Model):
page = models.CharField(
max_length=30,
verbose_name=_("associated page"),
db_index=True,
)
def get_bookmark(self, user):
userqueryset = MakeBookMark.objects.filter(bookmark=self, user=user)
if userqueryset.exists():
return True
else:
return False
def page_object(self):
try:
page = self.page
if page.startswith("p:"):
return Problem.objects.get(code=page[2:])
elif page.startswith("c:"):
return Contest.objects.get(key=page[2:])
elif page.startswith("b:"):
return BlogPost.objects.get(id=page[2:])
elif page.startswith("s:"):
return Solution.objects.get(problem__code=page[2:])
return None
except ObjectDoesNotExist:
return None
class Meta:
verbose_name = _("bookmark")
verbose_name_plural = _("bookmarks")
def __str__(self):
return self.page
class MakeBookMark(models.Model):
bookmark = models.ForeignKey(BookMark, related_name="bookmark", on_delete=CASCADE)
user = models.ForeignKey(Profile, related_name="user_bookmark", on_delete=CASCADE, db_index=True)
class Meta:
unique_together = ["user", "bookmark"]
verbose_name = _("make bookmark")
verbose_name_plural = _("make bookmarks")

View file

@ -8,6 +8,7 @@ from django.views.generic import ListView
from judge.comments import CommentedDetailView from judge.comments import CommentedDetailView
from judge.views.pagevote import PageVoteDetailView, PageVoteListView from judge.views.pagevote import PageVoteDetailView, PageVoteListView
from judge.views.bookmark import BookMarkDetailView, BookMarkListView
from judge.models import ( from judge.models import (
BlogPost, BlogPost,
Comment, Comment,
@ -93,7 +94,7 @@ class FeedView(ListView):
return context return context
class PostList(FeedView, PageVoteListView): class PostList(FeedView, PageVoteListView, BookMarkListView):
model = BlogPost model = BlogPost
paginate_by = 10 paginate_by = 10
context_object_name = "posts" context_object_name = "posts"
@ -128,7 +129,7 @@ class PostList(FeedView, PageVoteListView):
.order_by() .order_by()
} }
context = self.add_pagevote_context_data(context) context = self.add_pagevote_context_data(context)
context = self.add_bookmark_context_data(context)
return context return context
def get_comment_page(self, post): def get_comment_page(self, post):
@ -195,7 +196,7 @@ class CommentFeed(FeedView):
return context return context
class PostView(TitleMixin, CommentedDetailView, PageVoteDetailView): class PostView(TitleMixin, CommentedDetailView, PageVoteDetailView, BookMarkDetailView):
model = BlogPost model = BlogPost
pk_url_kwarg = "id" pk_url_kwarg = "id"
context_object_name = "post" context_object_name = "post"

82
judge/views/bookmark.py Normal file
View file

@ -0,0 +1,82 @@
from django.contrib.auth.decorators import login_required
from django.db import IntegrityError
from django.db.models import F
from django.http import (
Http404,
HttpResponse,
HttpResponseBadRequest,
HttpResponseForbidden,
)
from django.utils.translation import gettext as _
from judge.models.bookmark import BookMark, MakeBookMark
from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from judge.dblock import LockModel
from django.views.generic import View, ListView
__all__ = [
"dobookmark_page",
"undobookmark_page",
"BookMarkDetailView",
]
@login_required
def bookmark_page(request, delta):
if request.method != "POST":
return HttpResponseForbidden()
if "id" not in request.POST:
return HttpResponseBadRequest()
try:
bookmark_id = int(request.POST["id"])
bookmark_page = BookMark.objects.filter(id=bookmark_id)
except ValueError:
return HttpResponseBadRequest()
else:
if not bookmark_page.exists():
raise Http404()
if delta == 0:
bookmarklist = MakeBookMark.objects.filter(bookmark=bookmark_page.first(), user=request.profile)
if not bookmarklist.exists():
newbookmark = MakeBookMark(
bookmark=bookmark_page.first(),
user=request.profile,
)
newbookmark.save()
else:
bookmarklist = MakeBookMark.objects.filter(bookmark=bookmark_page.first(), user=request.profile)
if bookmarklist.exists():
bookmarklist.delete()
return HttpResponse("success", content_type="text/plain")
def dobookmark_page(request):
return bookmark_page(request, 0)
def undobookmark_page(request):
return bookmark_page(request, 1)
class BookMarkDetailView(TemplateResponseMixin, SingleObjectMixin, View):
def get_context_data(self, **kwargs):
context = super(BookMarkDetailView, self).get_context_data(**kwargs)
queryset = BookMark.objects.get_or_create(page=self.get_comment_page())
context["bookmark"] = queryset[0]
return context
class BookMarkListView(ListView):
def add_bookmark_context_data(self, context, obj_list="object_list"):
for item in context[obj_list]:
bookmark, _ = BookMark.objects.get_or_create(
page=self.get_comment_page(item)
)
setattr(item, "bookmark", bookmark)
return context

View file

@ -83,6 +83,7 @@ from judge.utils.views import (
) )
from judge.widgets import HeavyPreviewPageDownWidget from judge.widgets import HeavyPreviewPageDownWidget
from judge.views.pagevote import PageVoteDetailView from judge.views.pagevote import PageVoteDetailView
from judge.views.bookmark import BookMarkDetailView
__all__ = [ __all__ = [
@ -382,7 +383,7 @@ class ContestMixin(object):
) )
class ContestDetail(ContestMixin, TitleMixin, CommentedDetailView, PageVoteDetailView): class ContestDetail(ContestMixin, TitleMixin, CommentedDetailView, PageVoteDetailView, BookMarkDetailView):
template_name = "contest/contest.html" template_name = "contest/contest.html"
def get_comment_page(self): def get_comment_page(self):

View file

@ -72,6 +72,7 @@ from judge.views.problem import ProblemList
from judge.views.contests import ContestList from judge.views.contests import ContestList
from judge.views.submission import AllSubmissions, SubmissionsListBase from judge.views.submission import AllSubmissions, SubmissionsListBase
from judge.views.pagevote import PageVoteListView from judge.views.pagevote import PageVoteListView
from judge.views.bookmark import BookMarkListView
__all__ = [ __all__ = [
"OrganizationList", "OrganizationList",
@ -266,7 +267,7 @@ class OrganizationList(TitleMixin, ListView, OrganizationBase):
return context return context
class OrganizationHome(OrganizationDetailView, PageVoteListView): class OrganizationHome(OrganizationDetailView, PageVoteListView, BookMarkListView):
template_name = "organization/home.html" template_name = "organization/home.html"
pagevote_object_name = "posts" pagevote_object_name = "posts"
@ -294,6 +295,7 @@ class OrganizationHome(OrganizationDetailView, PageVoteListView):
context["title"] = self.object.name context["title"] = self.object.name
context["posts"], context["page_obj"] = self.get_posts_and_page_obj() context["posts"], context["page_obj"] = self.get_posts_and_page_obj()
context = self.add_pagevote_context_data(context, "posts") context = self.add_pagevote_context_data(context, "posts")
context = self.add_bookmark_context_data(context, "posts")
# Hack: This allows page_obj to have page_range for non-ListView class # Hack: This allows page_obj to have page_range for non-ListView class
setattr( setattr(

View file

@ -19,6 +19,8 @@ from django.views.generic import View, ListView
__all__ = [ __all__ = [
"upvote_page", "upvote_page",
"downvote_page", "downvote_page",
"PageVoteDetailView",
"PageVoteListView",
] ]
@ -97,21 +99,10 @@ class PageVoteDetailView(TemplateResponseMixin, SingleObjectMixin, View):
raise NotImplementedError() raise NotImplementedError()
return self.pagevote_page return self.pagevote_page
# def get(self, request, *args, **kwargs):
# self.object = self.get_object()
# return self.render_to_response(
# self.get_context_data(
# object=self.object,
# )
# )
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(PageVoteDetailView, self).get_context_data(**kwargs) context = super(PageVoteDetailView, self).get_context_data(**kwargs)
queryset = PageVote.objects.filter(page=self.get_comment_page()) queryset = PageVote.objects.get_or_create(page=self.get_comment_page())
if queryset.exists() == False: context["pagevote"] = queryset[0]
pagevote = PageVote(page=self.get_comment_page(), score=0)
pagevote.save()
context["pagevote"] = queryset.first()
return context return context

View file

@ -87,6 +87,7 @@ from judge.utils.views import (
) )
from judge.ml.collab_filter import CollabFilter from judge.ml.collab_filter import CollabFilter
from judge.views.pagevote import PageVoteDetailView, PageVoteListView from judge.views.pagevote import PageVoteDetailView, PageVoteListView
from judge.views.bookmark import BookMarkDetailView, BookMarkListView
def get_contest_problem(problem, profile): def get_contest_problem(problem, profile):
@ -178,6 +179,7 @@ class ProblemSolution(
TitleMixin, TitleMixin,
CommentedDetailView, CommentedDetailView,
PageVoteDetailView, PageVoteDetailView,
BookMarkDetailView,
): ):
context_object_name = "problem" context_object_name = "problem"
template_name = "problem/editorial.html" template_name = "problem/editorial.html"
@ -243,7 +245,7 @@ class ProblemRaw(
class ProblemDetail( class ProblemDetail(
ProblemMixin, SolvedProblemMixin, CommentedDetailView, PageVoteDetailView ProblemMixin, SolvedProblemMixin, CommentedDetailView, PageVoteDetailView, BookMarkDetailView
): ):
context_object_name = "problem" context_object_name = "problem"
template_name = "problem/problem.html" template_name = "problem/problem.html"
@ -813,7 +815,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
return HttpResponseRedirect(request.get_full_path()) return HttpResponseRedirect(request.get_full_path())
class ProblemFeed(ProblemList, PageVoteListView): class ProblemFeed(ProblemList, PageVoteListView, BookMarkListView):
model = Problem model = Problem
context_object_name = "problems" context_object_name = "problems"
template_name = "problem/feed.html" template_name = "problem/feed.html"

View file

@ -13,6 +13,7 @@ from django.db import transaction
from django.db.models import Count, Max, Min from django.db.models import Count, Max, Min
from django.db.models.fields import DateField from django.db.models.fields import DateField
from django.db.models.functions import Cast, ExtractYear from django.db.models.functions import Cast, ExtractYear
from judge.models.bookmark import MakeBookMark
from django.forms import Form from django.forms import Form
from django.http import ( from django.http import (
Http404, Http404,
@ -52,7 +53,7 @@ from judge.utils.views import (
) )
from .contests import ContestRanking from .contests import ContestRanking
__all__ = ["UserPage", "UserAboutPage", "UserProblemsPage", "users", "edit_profile"] __all__ = ["UserPage", "UserAboutPage", "UserProblemsPage", "UserBookMarkPage", "users", "edit_profile"]
def remap_keys(iterable, mapping): def remap_keys(iterable, mapping):
@ -349,6 +350,24 @@ class UserProblemsPage(UserPage):
return context return context
class UserBookMarkPage(UserPage):
template_name = "user/user-bookmarks.html"
def get_context_data(self, **kwargs):
context = super(UserBookMarkPage, self).get_context_data(**kwargs)
makedownlist = MakeBookMark.objects.filter(user=self.object)
pagelist = makedownlist.filter(bookmark__page__startswith='b')
problemlist = makedownlist.filter(bookmark__page__startswith='p')
contestlist = makedownlist.filter(bookmark__page__startswith='c')
context["pagelist"] = makedownlist
context["postlist"] = pagelist
context["problemlist"] = problemlist
context["contestlist"] = contestlist
return context
class UserPerformancePointsAjax(UserProblemsPage): class UserPerformancePointsAjax(UserProblemsPage):
template_name = "user/pp-table-body.html" template_name = "user/pp-table-body.html"

View file

@ -25,7 +25,10 @@
</span> </span>
{% endif %} {% endif %}
<span class="actionbar-block"> <span class="actionbar-block">
<span class="actionbar-button"> <span id="bookmark-button-{{bookmark.id}}"
class="bookmark-button actionbar-button {% if bookmark.get_bookmark(request.profile) == True %} bookmarked {% endif %}"
onclick="javascript:bookmark({{ bookmark.id }})"
>
<i class="fa fa-bookmark-o" style="font-size: large;"></i> <i class="fa fa-bookmark-o" style="font-size: large;"></i>
<span class="actionbar-text">{{_("Bookmark")}}</span> <span class="actionbar-text">{{_("Bookmark")}}</span>
</span> </span>

View file

@ -39,6 +39,10 @@
.actionbar-text { .actionbar-text {
padding-left: 0.4em; padding-left: 0.4em;
} }
.bookmarked {
color: rgb(180, 180, 7);
}
@media (max-width: 799px) { @media (max-width: 799px) {
.actionbar-text { .actionbar-text {
display: none; display: none;

View file

@ -20,6 +20,39 @@
}); });
} }
function ajax_bookmark(url, id, on_success) {
return $.ajax({
url: url,
type: 'POST',
data: {
id: id
},
success: function (data, textStatus, jqXHR) {
if (typeof on_success !== 'undefined')
on_success();
},
error: function (data, textStatus, jqXHR) {
alert('Could not bookmark: ' + data.responseText);
}
});
}
window.bookmark = function(id) {
var $bookmark = $('#bookmark-button-' + id);
if ($bookmark.hasClass('bookmarked')) {
ajax_bookmark('{{ url('undobookmark') }}', id, function () {
$bookmark.removeClass('bookmarked');
});
} else {
ajax_bookmark('{{ url('dobookmark') }}', id, function () {
if ($bookmark.hasClass('bookmarked'))
$bookmark.removeClass('bookmarked');
$bookmark.addClass('bookmarked');
});
}
}
var get_$votes = function (id) { var get_$votes = function (id) {
var $post = $('#page-vote-' + id); var $post = $('#page-vote-' + id);
return { return {
@ -30,7 +63,6 @@
window.pagevote_upvote = function (id) { window.pagevote_upvote = function (id) {
var $votes = get_$votes(id); var $votes = get_$votes(id);
console.log($votes.upvote, $votes.downvote);
if ($votes.upvote.hasClass('voted')) { if ($votes.upvote.hasClass('voted')) {
ajax_vote('{{ url('pagevote_downvote') }}', id, -1, function () { ajax_vote('{{ url('pagevote_downvote') }}', id, -1, function () {
$votes.upvote.removeClass('voted'); $votes.upvote.removeClass('voted');

View file

@ -43,6 +43,7 @@
{% endcache %} {% endcache %}
</div> </div>
{% set pagevote = post.pagevote %} {% set pagevote = post.pagevote %}
{% set bookmark = post.bookmark %}
{% set hide_actionbar_comment = True %} {% set hide_actionbar_comment = True %}
{% set include_hr = True %} {% set include_hr = True %}
{% include "actionbar/list.html" %} {% include "actionbar/list.html" %}

View file

@ -64,6 +64,7 @@
{% set include_hr = True %} {% set include_hr = True %}
{% set hide_actionbar_comment = True %} {% set hide_actionbar_comment = True %}
{% set pagevote = problem.pagevote %} {% set pagevote = problem.pagevote %}
{% set bookmark = post.bookmark %}
{% include "actionbar/list.html" %} {% include "actionbar/list.html" %}
{% if feed_type=='volunteer' and request.user.has_perm('judge.suggest_problem_changes') %} {% if feed_type=='volunteer' and request.user.has_perm('judge.suggest_problem_changes') %}

View file

@ -0,0 +1,90 @@
{% extends "user/user-base.html" %}
{% block title_ruler %}{% endblock %}
{% block title_row %}
{% set tab = 'bookmark' %}
{% include "user/user-tabs.html" %}
{% endblock %}
{% block user_content %}
{% if postlist %}
<div class="bookmark-group">
<h3 class="unselectable toggle closed">
<span class="fa fa-chevron-right fa-fw"></span>{{ _('Bookmarked Posts') }} ({{ postlist|length }})
</h3>
<table style="display: none" class="table toggled">
<thead>
<tr>
<th>{{ _('Post') }}</th>
</tr>
</thead>
<tbody>
{% for post in postlist %}
<tr>
<td class="bookmark-name">
<a href="{{ url('blog_post', post.bookmark.page_object().id, post.bookmark.page_object().slug) }}">{{ post.bookmark.page_object().title}} </a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<i>{{ _('You have not yet bookmarked any post.') }}</i>
{% endif %}
<hr>
{% if problemlist %}
<div class="bookmark-group">
<h3 class="unselectable toggle closed">
<span class="fa fa-chevron-right fa-fw"></span>{{ _('Bookmarked Problems') }} ({{ problemlist|length }})
</h3>
<table style="display: none" class="table toggled">
<thead>
<tr>
<th>{{ _('Problem') }}</th>
</tr>
</thead>
<tbody>
{% for problem in problemlist %}
<tr>
<td class="bookmark-name">
<a href="{{ url('problem_detail', problem.bookmark.page_object().code) }}">{{ problem.bookmark.page_object().name}} </a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<i>{{ _('You have not yet bookmarked any problem.') }}</i>
{% endif %}
<hr>
{% if contestlist %}
<div class="bookmark-group">
<h3 class="unselectable toggle closed">
<span class="fa fa-chevron-right fa-fw"></span>{{ _('Bookmarked Contests') }} ({{ contestlist|length }})
</h3>
<table style="display: none" class="table toggled">
<thead>
<tr>
<th>{{ _('Contest') }}</th>
</tr>
</thead>
<tbody>
{% for contest in contestlist %}
<tr>
<td class="bookmark-name">
<a href="{{ url('contest_view', contest.bookmark.page_object().key) }}">{{ contest.bookmark.page_object().name}} </a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<i>{{ _('You have not yet bookmarked any contest.') }}</i>
{% endif %}
<hr>
{% endblock %}

View file

@ -7,6 +7,7 @@
{{ make_tab('impersonate', 'fa-eye', url('impersonate-start', user.user.id), _('Impersonate')) }} {{ make_tab('impersonate', 'fa-eye', url('impersonate-start', user.user.id), _('Impersonate')) }}
{% endif %} {% endif %}
{% if user.user == request.user %} {% if user.user == request.user %}
{{ make_tab('bookmark', 'fa-bookmark', url('user_bookmark'), _('Bookmark')) }}
{{ make_tab('edit', 'fa-edit', url('user_edit_profile'), _('Edit profile')) }} {{ make_tab('edit', 'fa-edit', url('user_edit_profile'), _('Edit profile')) }}
{% else %} {% else %}
{% if perms.auth.change_user %} {% if perms.auth.change_user %}