wip: collection

This commit is contained in:
Your Name 2021-12-20 21:02:50 -05:00
parent 5fd1d67120
commit c7b25744f6
10 changed files with 371 additions and 2 deletions

View file

@ -27,6 +27,7 @@ urlpatterns = [
path('movies/', include('movies.urls')),
path('music/', include('music.urls')),
path('games/', include('games.urls')),
path('collections/', include('collection.urls')),
path('sync/', include('sync.urls')),
path('announcement/', include('management.urls')),
path('', include('common.urls')),

0
collection/__init__.py Normal file
View file

3
collection/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
collection/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CollectionConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'collection'

29
collection/forms.py Normal file
View file

@ -0,0 +1,29 @@
from django import forms
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.translation import gettext_lazy as _
from .models import Collection
from common.models import MarkStatusEnum
from common.forms import *
class CollectionForm(forms.ModelForm):
# id = forms.IntegerField(required=False, widget=forms.HiddenInput())
name = forms.CharField(label=_("标题"))
description = MarkdownxFormField(label=_("正文 (Markdown)"))
class Meta:
model = Collection
fields = [
'name',
'description',
'cover',
]
widgets = {
'name': forms.TextInput(attrs={'placeholder': _("收藏单名称")}),
'developer': forms.TextInput(attrs={'placeholder': _("多个开发商使用英文逗号分隔")}),
'publisher': forms.TextInput(attrs={'placeholder': _("多个发行商使用英文逗号分隔")}),
'genre': forms.TextInput(attrs={'placeholder': _("多个类型使用英文逗号分隔")}),
'platform': forms.TextInput(attrs={'placeholder': _("多个平台使用英文逗号分隔")}),
'cover': PreviewImageInput(),
}

47
collection/models.py Normal file
View file

@ -0,0 +1,47 @@
from django.db import models
from common.models import UserOwnedEntity
from movies.models import Movie
from books.models import Book
from music.models import Song, Album
from games.models import Game
from markdownx.models import MarkdownxField
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
def collection_cover_path(instance, filename):
return GenerateDateUUIDMediaFilePath(instance, filename, settings.COLLECTION_MEDIA_PATH_ROOT)
class Collection(UserOwnedEntity):
name = models.CharField(max_length=200)
description = MarkdownxField()
cover = models.ImageField(_("封面"), upload_to=collection_cover_path, default=settings.DEFAULT_COLLECTION_IMAGE, blank=True)
def __str__(self):
return str(self.owner) + ': ' + self.name
@property
def item_list(self):
return list(self.collectionitem_set.objects.all()).sort(lambda i: i.position)
@property
def plain_description(self):
html = markdown(self.description)
return RE_HTML_TAG.sub(' ', html)
class CollectionItem(models.Model):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, null=True)
album = models.ForeignKey(Album, on_delete=models.CASCADE, null=True)
song = models.ForeignKey(Song, on_delete=models.CASCADE, null=True)
book = models.ForeignKey(Book, on_delete=models.CASCADE, null=True)
game = models.ForeignKey(Game, on_delete=models.CASCADE, null=True)
collection = models.ForeignKey(Collection, on_delete=models.CASCADE)
position = models.PositiveIntegerField()
comment = models.TextField(_("备注"), default='')
@property
def item(self):
items = list(filter(lambda i: i is not None, [self.movie, self.book, self.album, self.song, self.game]))
return items[0] if len(items) > 0 else None

3
collection/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

15
collection/urls.py Normal file
View file

@ -0,0 +1,15 @@
from django.urls import path, re_path
from .views import *
app_name = 'collection'
urlpatterns = [
path('create/', create, name='create'),
path('<int:id>/', retrieve, name='retrieve'),
path('update/<int:id>/', update, name='update'),
path('delete/<int:id>/', delete, name='delete'),
path('follow/<int:id>/', follow, name='follow'),
path('unfollow/<int:id>/', follow, name='unfollow'),
path('with/<str:type>/<int:id>/', list_with, name='list_with'),
# TODO: tag
]

266
collection/views.py Normal file
View file

@ -0,0 +1,266 @@
import logging
from django.shortcuts import render, get_object_or_404, redirect, reverse
from django.contrib.auth.decorators import login_required, permission_required
from django.utils.translation import gettext_lazy as _
from django.http import HttpResponseBadRequest, HttpResponseServerError
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import IntegrityError, transaction
from django.db.models import Count
from django.utils import timezone
from django.core.paginator import Paginator
from mastodon import mastodon_request_included
from mastodon.models import MastodonApplication
from mastodon.api import post_toot, TootVisibilityEnum
from mastodon.utils import rating_to_emoji
from common.utils import PageLinksGenerator
from common.views import PAGE_LINK_NUMBER, jump_or_scrape
from common.models import SourceSiteEnum
from .models import *
from .forms import *
from django.conf import settings
logger = logging.getLogger(__name__)
mastodon_logger = logging.getLogger("django.mastodon")
# how many marks showed on the detail page
MARK_NUMBER = 5
# how many marks at the mark page
MARK_PER_PAGE = 20
# how many reviews showed on the detail page
REVIEW_NUMBER = 5
# how many reviews at the mark page
REVIEW_PER_PAGE = 20
# max tags on detail page
TAG_NUMBER = 10
# public data
###########################
@login_required
def create(request):
if request.method == 'GET':
form = GameForm()
return render(
request,
'games/create_update.html',
{
'form': form,
'title': _('添加游戏'),
'submit_url': reverse("games:create"),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
elif request.method == 'POST':
if request.user.is_authenticated:
# only local user can alter public data
form = GameForm(request.POST, request.FILES)
if form.is_valid():
form.instance.last_editor = request.user
try:
with transaction.atomic():
form.save()
if form.instance.source_site == SourceSiteEnum.IN_SITE.value:
real_url = form.instance.get_absolute_url()
form.instance.source_url = real_url
form.instance.save()
except IntegrityError as e:
logger.error(e.__str__())
return HttpResponseServerError("integrity error")
return redirect(reverse("games:retrieve", args=[form.instance.id]))
else:
return render(
request,
'games/create_update.html',
{
'form': form,
'title': _('添加游戏'),
'submit_url': reverse("games:create"),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
else:
return redirect(reverse("users:login"))
else:
return HttpResponseBadRequest()
@login_required
def update(request, id):
if request.method == 'GET':
game = get_object_or_404(Game, pk=id)
form = GameForm(instance=game)
page_title = _('修改游戏')
return render(
request,
'games/create_update.html',
{
'form': form,
'title': page_title,
'submit_url': reverse("games:update", args=[game.id]),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
elif request.method == 'POST':
game = get_object_or_404(Game, pk=id)
form = GameForm(request.POST, request.FILES, instance=game)
page_title = _("修改游戏")
if form.is_valid():
form.instance.last_editor = request.user
form.instance.edited_time = timezone.now()
try:
with transaction.atomic():
form.save()
if form.instance.source_site == SourceSiteEnum.IN_SITE.value:
real_url = form.instance.get_absolute_url()
form.instance.source_url = real_url
form.instance.save()
except IntegrityError as e:
logger.error(e.__str__())
return HttpResponseServerError("integrity error")
else:
return render(
request,
'games/create_update.html',
{
'form': form,
'title': page_title,
'submit_url': reverse("games:update", args=[game.id]),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
return redirect(reverse("games:retrieve", args=[form.instance.id]))
else:
return HttpResponseBadRequest()
@mastodon_request_included
# @login_required
def retrieve(request, id):
if request.method == 'GET':
game = get_object_or_404(Game, pk=id)
mark = None
mark_tags = None
review = None
# retreive tags
game_tag_list = game.game_tags.values('content').annotate(
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER]
# retrieve user mark and initialize mark form
try:
if request.user.is_authenticated:
mark = GameMark.objects.get(owner=request.user, game=game)
except ObjectDoesNotExist:
mark = None
if mark:
mark_tags = mark.gamemark_tags.all()
mark.get_status_display = GameMarkStatusTranslator(mark.status)
mark_form = GameMarkForm(instance=mark, initial={
'tags': mark_tags
})
else:
mark_form = GameMarkForm(initial={
'game': game,
'tags': mark_tags
})
# retrieve user review
try:
if request.user.is_authenticated:
review = GameReview.objects.get(
owner=request.user, game=game)
except ObjectDoesNotExist:
review = None
# retrieve other related reviews and marks
if request.user.is_anonymous:
# hide all marks and reviews for anonymous user
mark_list = None
review_list = None
mark_list_more = None
review_list_more = None
else:
mark_list = GameMark.get_available(game, request.user)
review_list = GameReview.get_available(game, request.user)
mark_list_more = True if len(mark_list) > MARK_NUMBER else False
mark_list = mark_list[:MARK_NUMBER]
for m in mark_list:
m.get_status_display = GameMarkStatusTranslator(m.status)
review_list_more = True if len(
review_list) > REVIEW_NUMBER else False
review_list = review_list[:REVIEW_NUMBER]
# def strip_html_tags(text):
# import re
# regex = re.compile('<.*?>')
# return re.sub(regex, '', text)
# for r in review_list:
# r.content = strip_html_tags(r.content)
return render(
request,
'games/detail.html',
{
'game': game,
'mark': mark,
'review': review,
'status_enum': MarkStatusEnum,
'mark_form': mark_form,
'mark_list': mark_list,
'mark_list_more': mark_list_more,
'review_list': review_list,
'review_list_more': review_list_more,
'game_tag_list': game_tag_list,
'mark_tags': mark_tags,
}
)
else:
logger.warning('non-GET method at /games/<id>')
return HttpResponseBadRequest()
@permission_required("games.delete_game")
@login_required
def delete(request, id):
if request.method == 'GET':
game = get_object_or_404(Game, pk=id)
return render(
request,
'games/delete.html',
{
'game': game,
}
)
elif request.method == 'POST':
if request.user.is_staff:
# only staff has right to delete
game = get_object_or_404(Game, pk=id)
game.delete()
return redirect(reverse("common:home"))
else:
raise PermissionDenied()
else:
return HttpResponseBadRequest()
@login_required
def follow(request, id):
pass
@login_required
def unfollow(request, id):
pass
@login_required
def list_with(request, type, id):
pass

View file

@ -152,8 +152,7 @@ class Entity(models.Model):
class UserOwnedEntity(models.Model):
is_private = models.BooleanField(default=False, null=True) # first set allow null, then migration, finally (in a few days) remove for good
visibility = models.PositiveSmallIntegerField(default=0) # 0: Public / 1: Follower only / 2: Self only
owner = models.ForeignKey(
User, on_delete=models.CASCADE, related_name='user_%(class)ss')
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_%(class)ss')
created_time = models.DateTimeField(default=timezone.now)
edited_time = models.DateTimeField(default=timezone.now)