diff --git a/dmoj/urls.py b/dmoj/urls.py index 353fdcb..9103a6a 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -60,6 +60,7 @@ from judge.views import ( user, volunteer, pagevote, + bookmark, widgets, internal, ) @@ -403,6 +404,7 @@ urlpatterns = [ ), url(r"^user$", user.UserAboutPage.as_view(), name="user_page"), url(r"^edit/profile/$", user.edit_profile, name="user_edit_profile"), + url(r"^user/bookmarks", user.UserBookMarkPage.as_view(), name="user_bookmark"), url( r"^user/(?P\w+)", include( @@ -448,6 +450,8 @@ urlpatterns = [ ), url(r"^pagevotes/upvote/$", pagevote.upvote_page, name="pagevote_upvote"), 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/downvote/$", comment.downvote_comment, name="comment_downvote"), url(r"^comments/hide/$", comment.comment_hide, name="comment_hide"), diff --git a/judge/migrations/0138_bookmark_makebookmark.py b/judge/migrations/0138_bookmark_makebookmark.py new file mode 100644 index 0000000..c730148 --- /dev/null +++ b/judge/migrations/0138_bookmark_makebookmark.py @@ -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')}, + }, + ), + ] diff --git a/judge/models/__init__.py b/judge/models/__init__.py index aac74f0..39ce95e 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -55,6 +55,7 @@ from judge.models.submission import ( from judge.models.ticket import Ticket, TicketMessage from judge.models.volunteer import VolunteerProblemVote 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(Problem, follow=["language_limits"]) @@ -79,4 +80,5 @@ revisions.register(ContestParticipation) revisions.register(Rating) revisions.register(PageVoteVoter) revisions.register(VolunteerProblemVote) +revisions.register(MakeBookMark) del revisions diff --git a/judge/models/bookmark.py b/judge/models/bookmark.py new file mode 100644 index 0000000..6945719 --- /dev/null +++ b/judge/models/bookmark.py @@ -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") diff --git a/judge/views/blog.py b/judge/views/blog.py index 89cc151..2bcc0a6 100644 --- a/judge/views/blog.py +++ b/judge/views/blog.py @@ -8,6 +8,7 @@ from django.views.generic import ListView from judge.comments import CommentedDetailView from judge.views.pagevote import PageVoteDetailView, PageVoteListView +from judge.views.bookmark import BookMarkDetailView, BookMarkListView from judge.models import ( BlogPost, Comment, @@ -93,7 +94,7 @@ class FeedView(ListView): return context -class PostList(FeedView, PageVoteListView): +class PostList(FeedView, PageVoteListView, BookMarkListView): model = BlogPost paginate_by = 10 context_object_name = "posts" @@ -128,7 +129,7 @@ class PostList(FeedView, PageVoteListView): .order_by() } context = self.add_pagevote_context_data(context) - + context = self.add_bookmark_context_data(context) return context def get_comment_page(self, post): @@ -195,7 +196,7 @@ class CommentFeed(FeedView): return context -class PostView(TitleMixin, CommentedDetailView, PageVoteDetailView): +class PostView(TitleMixin, CommentedDetailView, PageVoteDetailView, BookMarkDetailView): model = BlogPost pk_url_kwarg = "id" context_object_name = "post" diff --git a/judge/views/bookmark.py b/judge/views/bookmark.py new file mode 100644 index 0000000..f4cf85f --- /dev/null +++ b/judge/views/bookmark.py @@ -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 \ No newline at end of file diff --git a/judge/views/contests.py b/judge/views/contests.py index 3fc64e6..161e11a 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -83,6 +83,7 @@ from judge.utils.views import ( ) from judge.widgets import HeavyPreviewPageDownWidget from judge.views.pagevote import PageVoteDetailView +from judge.views.bookmark import BookMarkDetailView __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" def get_comment_page(self): diff --git a/judge/views/organization.py b/judge/views/organization.py index 486bf77..fd2e2fa 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -72,6 +72,7 @@ from judge.views.problem import ProblemList from judge.views.contests import ContestList from judge.views.submission import AllSubmissions, SubmissionsListBase from judge.views.pagevote import PageVoteListView +from judge.views.bookmark import BookMarkListView __all__ = [ "OrganizationList", @@ -266,7 +267,7 @@ class OrganizationList(TitleMixin, ListView, OrganizationBase): return context -class OrganizationHome(OrganizationDetailView, PageVoteListView): +class OrganizationHome(OrganizationDetailView, PageVoteListView, BookMarkListView): template_name = "organization/home.html" pagevote_object_name = "posts" @@ -294,6 +295,7 @@ class OrganizationHome(OrganizationDetailView, PageVoteListView): context["title"] = self.object.name context["posts"], context["page_obj"] = self.get_posts_and_page_obj() 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 setattr( diff --git a/judge/views/pagevote.py b/judge/views/pagevote.py index f749978..e01f664 100644 --- a/judge/views/pagevote.py +++ b/judge/views/pagevote.py @@ -19,6 +19,8 @@ from django.views.generic import View, ListView __all__ = [ "upvote_page", "downvote_page", + "PageVoteDetailView", + "PageVoteListView", ] @@ -97,21 +99,10 @@ class PageVoteDetailView(TemplateResponseMixin, SingleObjectMixin, View): raise NotImplementedError() 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): context = super(PageVoteDetailView, self).get_context_data(**kwargs) - queryset = PageVote.objects.filter(page=self.get_comment_page()) - if queryset.exists() == False: - pagevote = PageVote(page=self.get_comment_page(), score=0) - pagevote.save() - context["pagevote"] = queryset.first() + queryset = PageVote.objects.get_or_create(page=self.get_comment_page()) + context["pagevote"] = queryset[0] return context diff --git a/judge/views/problem.py b/judge/views/problem.py index 8b922b6..7c61082 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -87,6 +87,7 @@ from judge.utils.views import ( ) from judge.ml.collab_filter import CollabFilter from judge.views.pagevote import PageVoteDetailView, PageVoteListView +from judge.views.bookmark import BookMarkDetailView, BookMarkListView def get_contest_problem(problem, profile): @@ -178,6 +179,7 @@ class ProblemSolution( TitleMixin, CommentedDetailView, PageVoteDetailView, + BookMarkDetailView, ): context_object_name = "problem" template_name = "problem/editorial.html" @@ -243,7 +245,7 @@ class ProblemRaw( class ProblemDetail( - ProblemMixin, SolvedProblemMixin, CommentedDetailView, PageVoteDetailView + ProblemMixin, SolvedProblemMixin, CommentedDetailView, PageVoteDetailView, BookMarkDetailView ): context_object_name = "problem" template_name = "problem/problem.html" @@ -813,7 +815,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView return HttpResponseRedirect(request.get_full_path()) -class ProblemFeed(ProblemList, PageVoteListView): +class ProblemFeed(ProblemList, PageVoteListView, BookMarkListView): model = Problem context_object_name = "problems" template_name = "problem/feed.html" diff --git a/judge/views/user.py b/judge/views/user.py index da4120a..4832be8 100644 --- a/judge/views/user.py +++ b/judge/views/user.py @@ -13,6 +13,7 @@ from django.db import transaction from django.db.models import Count, Max, Min from django.db.models.fields import DateField from django.db.models.functions import Cast, ExtractYear +from judge.models.bookmark import MakeBookMark from django.forms import Form from django.http import ( Http404, @@ -52,7 +53,7 @@ from judge.utils.views import ( ) from .contests import ContestRanking -__all__ = ["UserPage", "UserAboutPage", "UserProblemsPage", "users", "edit_profile"] +__all__ = ["UserPage", "UserAboutPage", "UserProblemsPage", "UserBookMarkPage", "users", "edit_profile"] def remap_keys(iterable, mapping): @@ -349,6 +350,24 @@ class UserProblemsPage(UserPage): 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): template_name = "user/pp-table-body.html" diff --git a/templates/actionbar/list.html b/templates/actionbar/list.html index d0b2ca8..da0c078 100644 --- a/templates/actionbar/list.html +++ b/templates/actionbar/list.html @@ -25,7 +25,10 @@ {% endif %} - + {{_("Bookmark")}} diff --git a/templates/actionbar/media-css.html b/templates/actionbar/media-css.html index 60f75c8..64c8294 100644 --- a/templates/actionbar/media-css.html +++ b/templates/actionbar/media-css.html @@ -39,6 +39,10 @@ .actionbar-text { padding-left: 0.4em; } + .bookmarked { + color: rgb(180, 180, 7); + } + @media (max-width: 799px) { .actionbar-text { display: none; diff --git a/templates/actionbar/media-js.html b/templates/actionbar/media-js.html index 53f08fa..9144ea1 100644 --- a/templates/actionbar/media-js.html +++ b/templates/actionbar/media-js.html @@ -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 $post = $('#page-vote-' + id); return { @@ -30,7 +63,6 @@ window.pagevote_upvote = function (id) { var $votes = get_$votes(id); - console.log($votes.upvote, $votes.downvote); if ($votes.upvote.hasClass('voted')) { ajax_vote('{{ url('pagevote_downvote') }}', id, -1, function () { $votes.upvote.removeClass('voted'); diff --git a/templates/blog/content.html b/templates/blog/content.html index 6d40994..ba754f0 100644 --- a/templates/blog/content.html +++ b/templates/blog/content.html @@ -43,6 +43,7 @@ {% endcache %} {% set pagevote = post.pagevote %} + {% set bookmark = post.bookmark %} {% set hide_actionbar_comment = True %} {% set include_hr = True %} {% include "actionbar/list.html" %} diff --git a/templates/problem/feed.html b/templates/problem/feed.html index 9933d55..44bbf70 100644 --- a/templates/problem/feed.html +++ b/templates/problem/feed.html @@ -64,6 +64,7 @@ {% set include_hr = True %} {% set hide_actionbar_comment = True %} {% set pagevote = problem.pagevote %} + {% set bookmark = post.bookmark %} {% include "actionbar/list.html" %} {% if feed_type=='volunteer' and request.user.has_perm('judge.suggest_problem_changes') %} diff --git a/templates/user/user-bookmarks.html b/templates/user/user-bookmarks.html new file mode 100644 index 0000000..9ba9215 --- /dev/null +++ b/templates/user/user-bookmarks.html @@ -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 %} +
+

+ {{ _('Bookmarked Posts') }} ({{ postlist|length }}) +

+ + + + + + + + {% for post in postlist %} + + + + {% endfor %} + + +
+ {% else %} + {{ _('You have not yet bookmarked any post.') }} + {% endif %} +
+ {% if problemlist %} +
+

+ {{ _('Bookmarked Problems') }} ({{ problemlist|length }}) +

+ + + + + + + + {% for problem in problemlist %} + + + + {% endfor %} + + +
+ {% else %} + {{ _('You have not yet bookmarked any problem.') }} + {% endif %} +
+ {% if contestlist %} +
+

+ {{ _('Bookmarked Contests') }} ({{ contestlist|length }}) +

+ + + + + + + + {% for contest in contestlist %} + + + + {% endfor %} + + +
+ {% else %} + {{ _('You have not yet bookmarked any contest.') }} + {% endif %} +
+ +{% endblock %} diff --git a/templates/user/user-tabs.html b/templates/user/user-tabs.html index 1fab2e6..096a45b 100644 --- a/templates/user/user-tabs.html +++ b/templates/user/user-tabs.html @@ -7,6 +7,7 @@ {{ make_tab('impersonate', 'fa-eye', url('impersonate-start', user.user.id), _('Impersonate')) }} {% endif %} {% 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')) }} {% else %} {% if perms.auth.change_user %}