wip: collection
This commit is contained in:
parent
5fd1d67120
commit
c7b25744f6
10 changed files with 371 additions and 2 deletions
|
@ -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
0
collection/__init__.py
Normal file
3
collection/admin.py
Normal file
3
collection/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
collection/apps.py
Normal file
6
collection/apps.py
Normal 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
29
collection/forms.py
Normal 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
47
collection/models.py
Normal 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
3
collection/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
15
collection/urls.py
Normal file
15
collection/urls.py
Normal 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
266
collection/views.py
Normal 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
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue