Add ability to manage long plays
This commit is contained in:
@ -12,7 +12,7 @@ from django.core.files.base import ContentFile
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
from scrobbles.mixins import ScrobblableMixin
|
from scrobbles.mixins import LongPlayScrobblableMixin, ScrobblableMixin
|
||||||
from scrobbles.utils import get_scrobbles_for_media
|
from scrobbles.utils import get_scrobbles_for_media
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -55,7 +55,7 @@ class Author(TimeStampedModel):
|
|||||||
self.headshot.save(fname, ContentFile(r.content), save=True)
|
self.headshot.save(fname, ContentFile(r.content), save=True)
|
||||||
|
|
||||||
|
|
||||||
class Book(ScrobblableMixin):
|
class Book(LongPlayScrobblableMixin):
|
||||||
COMPLETION_PERCENT = getattr(settings, "BOOK_COMPLETION_PERCENT", 95)
|
COMPLETION_PERCENT = getattr(settings, "BOOK_COMPLETION_PERCENT", 95)
|
||||||
AVG_PAGE_READING_SECONDS = getattr(
|
AVG_PAGE_READING_SECONDS = getattr(
|
||||||
settings, "AVERAGE_PAGE_READING_SECONDS", 60
|
settings, "AVERAGE_PAGE_READING_SECONDS", 60
|
||||||
@ -78,6 +78,16 @@ class Book(ScrobblableMixin):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.title} by {self.author}"
|
return f"{self.title} by {self.author}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subtitle(self):
|
||||||
|
return f" by {self.author}"
|
||||||
|
|
||||||
|
def get_start_url(self):
|
||||||
|
return reverse("scrobbles:start", kwargs={"uuid": self.uuid})
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("books:book_detail", kwargs={"slug": self.uuid})
|
||||||
|
|
||||||
def fix_metadata(self, force_update=False):
|
def fix_metadata(self, force_update=False):
|
||||||
if not self.openlibrary_id or force_update:
|
if not self.openlibrary_id or force_update:
|
||||||
book_dict = lookup_book_from_openlibrary(self.title, self.author)
|
book_dict = lookup_book_from_openlibrary(self.title, self.author)
|
||||||
@ -130,9 +140,6 @@ class Book(ScrobblableMixin):
|
|||||||
def author(self):
|
def author(self):
|
||||||
return self.authors.first()
|
return self.authors.first()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("books:book_detail", kwargs={"slug": self.uuid})
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pages_for_completion(self) -> int:
|
def pages_for_completion(self) -> int:
|
||||||
if not self.pages:
|
if not self.pages:
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
|
from scrobbles.utils import get_scrobbles_for_media
|
||||||
|
|
||||||
BNULL = {"blank": True, "null": True}
|
BNULL = {"blank": True, "null": True}
|
||||||
|
|
||||||
@ -27,3 +29,19 @@ class ScrobblableMixin(TimeStampedModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(cls):
|
def find_or_create(cls):
|
||||||
logger.warn("find_or_create() not implemented yet")
|
logger.warn("find_or_create() not implemented yet")
|
||||||
|
|
||||||
|
|
||||||
|
class LongPlayScrobblableMixin(ScrobblableMixin):
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def get_longplay_finish_url(self):
|
||||||
|
return reverse("scrobbles:longplay-finish", kwargs={"uuid": self.uuid})
|
||||||
|
|
||||||
|
def last_long_play_scrobble_for_user(self, user) -> Optional["Scrobble"]:
|
||||||
|
return (
|
||||||
|
get_scrobbles_for_media(self, user)
|
||||||
|
.filter(long_play_complete=False)
|
||||||
|
.order_by("-timestamp")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|||||||
@ -222,21 +222,11 @@ def manual_scrobble_video_game(data_dict: dict, user_id: Optional[int]):
|
|||||||
def manual_scrobble_book(data_dict: dict, user_id: Optional[int]):
|
def manual_scrobble_book(data_dict: dict, user_id: Optional[int]):
|
||||||
book = Book.find_or_create(data_dict)
|
book = Book.find_or_create(data_dict)
|
||||||
|
|
||||||
last_scrobble = Scrobble.objects.filter(
|
|
||||||
book=book,
|
|
||||||
user_id=user_id,
|
|
||||||
played_to_completion=True,
|
|
||||||
long_play_complete=False,
|
|
||||||
).last()
|
|
||||||
|
|
||||||
start_playback_position = 0
|
|
||||||
if last_scrobble:
|
|
||||||
start_playback_position = last_scrobble.playback_position or 0
|
|
||||||
scrobble_dict = {
|
scrobble_dict = {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"timestamp": timezone.now(),
|
"timestamp": timezone.now(),
|
||||||
"playback_position_ticks": int(start_playback_position) * 1000,
|
"playback_position_ticks": None,
|
||||||
"playback_position": start_playback_position,
|
"playback_position": 0,
|
||||||
"source": "Vrobbler",
|
"source": "Vrobbler",
|
||||||
"long_play_complete": False,
|
"long_play_complete": False,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,14 @@ urlpatterns = [
|
|||||||
views.KoReaderImportCreateView.as_view(),
|
views.KoReaderImportCreateView.as_view(),
|
||||||
name="koreader-file-upload",
|
name="koreader-file-upload",
|
||||||
),
|
),
|
||||||
path("finish/<slug:uuid>", views.scrobble_finish, name="finish"),
|
path("start/<slug:uuid>/", views.scrobble_start, name="start"),
|
||||||
path("cancel/<slug:uuid>", views.scrobble_cancel, name="cancel"),
|
path("finish/<slug:uuid>/", views.scrobble_finish, name="finish"),
|
||||||
|
path("cancel/<slug:uuid>/", views.scrobble_cancel, name="cancel"),
|
||||||
|
path(
|
||||||
|
"long-play-finish/<slug:uuid>/",
|
||||||
|
views.scrobble_longplay_finish,
|
||||||
|
name="longplay-finish",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"upload/",
|
"upload/",
|
||||||
views.AudioScrobblerImportCreateView.as_view(),
|
views.AudioScrobblerImportCreateView.as_view(),
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import logging
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
@ -31,6 +32,7 @@ from scrobbles.api import serializers
|
|||||||
from scrobbles.constants import (
|
from scrobbles.constants import (
|
||||||
JELLYFIN_AUDIO_ITEM_TYPES,
|
JELLYFIN_AUDIO_ITEM_TYPES,
|
||||||
JELLYFIN_VIDEO_ITEM_TYPES,
|
JELLYFIN_VIDEO_ITEM_TYPES,
|
||||||
|
LONG_PLAY_MEDIA,
|
||||||
)
|
)
|
||||||
from scrobbles.export import export_scrobbles
|
from scrobbles.export import export_scrobbles
|
||||||
from scrobbles.forms import ExportScrobbleForm, ScrobbleForm
|
from scrobbles.forms import ExportScrobbleForm, ScrobbleForm
|
||||||
@ -60,8 +62,8 @@ from sports.thesportsdb import lookup_event_from_thesportsdb
|
|||||||
from videogames.howlongtobeat import lookup_game_from_hltb
|
from videogames.howlongtobeat import lookup_game_from_hltb
|
||||||
from videos.imdb import lookup_video_from_imdb
|
from videos.imdb import lookup_video_from_imdb
|
||||||
|
|
||||||
from vrobbler.apps.books.openlibrary import lookup_book_from_openlibrary
|
from books.openlibrary import lookup_book_from_openlibrary
|
||||||
from vrobbler.apps.scrobbles.utils import (
|
from scrobbles.utils import (
|
||||||
get_long_plays_completed,
|
get_long_plays_completed,
|
||||||
get_long_plays_in_progress,
|
get_long_plays_in_progress,
|
||||||
)
|
)
|
||||||
@ -388,6 +390,84 @@ def import_audioscrobbler_file(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
@api_view(["GET"])
|
||||||
|
def scrobble_start(request, uuid):
|
||||||
|
user = request.user
|
||||||
|
success_url = request.META.get("HTTP_REFERER")
|
||||||
|
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return HttpResponseRedirect(success_url)
|
||||||
|
|
||||||
|
media_obj = None
|
||||||
|
for app, model in LONG_PLAY_MEDIA.items():
|
||||||
|
media_model = apps.get_model(app_label=app, model_name=model)
|
||||||
|
media_obj = media_model.objects.filter(uuid=uuid).first()
|
||||||
|
if media_obj:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not media_obj:
|
||||||
|
return
|
||||||
|
|
||||||
|
scrobble = None
|
||||||
|
user_id = request.user.id
|
||||||
|
if media_obj and media_obj.__class__.__name__ == "Book":
|
||||||
|
data_dict = {
|
||||||
|
"title": media_obj.title,
|
||||||
|
"author": media_obj.author.name,
|
||||||
|
}
|
||||||
|
scrobble = manual_scrobble_book(data_dict, user_id)
|
||||||
|
if media_obj and media_obj.__class__.__name__ == "VideoGame":
|
||||||
|
data_dict = {"hltb_id": media_obj.hltb_id}
|
||||||
|
scrobble = manual_scrobble_video_game(data_dict, user_id)
|
||||||
|
|
||||||
|
if scrobble:
|
||||||
|
messages.add_message(
|
||||||
|
request,
|
||||||
|
messages.SUCCESS,
|
||||||
|
f"Scrobble of {scrobble.media_obj} started.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.add_message(
|
||||||
|
request, messages.ERROR, f"Media with uuid {uuid} not found."
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(success_url)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["GET"])
|
||||||
|
def scrobble_longplay_finish(request, uuid):
|
||||||
|
user = request.user
|
||||||
|
success_url = request.META.get("HTTP_REFERER")
|
||||||
|
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return HttpResponseRedirect(success_url)
|
||||||
|
|
||||||
|
media_obj = None
|
||||||
|
for app, model in LONG_PLAY_MEDIA.items():
|
||||||
|
media_model = apps.get_model(app_label=app, model_name=model)
|
||||||
|
media_obj = media_model.objects.filter(uuid=uuid).first()
|
||||||
|
if media_obj:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not media_obj:
|
||||||
|
return
|
||||||
|
|
||||||
|
last_scrobble = media_obj.last_long_play_scrobble_for_user(user)
|
||||||
|
if last_scrobble and last_scrobble.long_play_complete == False:
|
||||||
|
last_scrobble.long_play_complete = True
|
||||||
|
last_scrobble.save(update_fields=["long_play_complete"])
|
||||||
|
messages.add_message(
|
||||||
|
request,
|
||||||
|
messages.SUCCESS,
|
||||||
|
f"Long play of {media_obj} finished.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.add_message(
|
||||||
|
request, messages.ERROR, f"Media with uuid {uuid} not found."
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(success_url)
|
||||||
|
|
||||||
|
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
def scrobble_finish(request, uuid):
|
def scrobble_finish(request, uuid):
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -7,7 +6,7 @@ from django.contrib.auth import get_user_model
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
from scrobbles.mixins import ScrobblableMixin
|
from scrobbles.mixins import LongPlayScrobblableMixin
|
||||||
from scrobbles.utils import get_scrobbles_for_media
|
from scrobbles.utils import get_scrobbles_for_media
|
||||||
from videogames.igdb import lookup_game_id_from_gdb
|
from videogames.igdb import lookup_game_id_from_gdb
|
||||||
|
|
||||||
@ -46,7 +45,7 @@ class VideoGameCollection(TimeStampedModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class VideoGame(ScrobblableMixin):
|
class VideoGame(LongPlayScrobblableMixin):
|
||||||
COMPLETION_PERCENT = getattr(settings, "GAME_COMPLETION_PERCENT", 100)
|
COMPLETION_PERCENT = getattr(settings, "GAME_COMPLETION_PERCENT", 100)
|
||||||
|
|
||||||
FIELDS_FROM_IGDB = [
|
FIELDS_FROM_IGDB = [
|
||||||
@ -89,6 +88,10 @@ class VideoGame(ScrobblableMixin):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subtitle(self):
|
||||||
|
return f" On {self.platforms.first()}"
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
"videogames:videogame_detail", kwargs={"slug": self.uuid}
|
"videogames:videogame_detail", kwargs={"slug": self.uuid}
|
||||||
@ -101,6 +104,9 @@ class VideoGame(ScrobblableMixin):
|
|||||||
slug = self.title.lower().replace(" ", "-")
|
slug = self.title.lower().replace(" ", "-")
|
||||||
return f"https://igdb.com/games/{slug}"
|
return f"https://igdb.com/games/{slug}"
|
||||||
|
|
||||||
|
def get_start_url(self):
|
||||||
|
return reverse("scrobbles:start", kwargs={"uuid": self.uuid})
|
||||||
|
|
||||||
def save(self, **kwargs):
|
def save(self, **kwargs):
|
||||||
super().save(**kwargs)
|
super().save(**kwargs)
|
||||||
self.fix_metadata()
|
self.fix_metadata()
|
||||||
|
|||||||
@ -31,11 +31,13 @@
|
|||||||
min-height: 3em;
|
min-height: 3em;
|
||||||
border-right: 1px solid #777;
|
border-right: 1px solid #777;
|
||||||
}
|
}
|
||||||
|
.now-playing { margin-right:20px;}
|
||||||
.now-playing p { margin:0; }
|
.now-playing p { margin:0; }
|
||||||
.now-playing .right { float:right; margin-right:10px; }
|
.now-playing .right { float:right; margin-right:10px; }
|
||||||
.latest-scrobble {
|
.latest-scrobble {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
.now-playing img { height:75px; width: 75px; object-fit: cover; }
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -244,13 +246,18 @@
|
|||||||
{% for scrobble in now_playing_list %}
|
{% for scrobble in now_playing_list %}
|
||||||
<div class="now-playing">
|
<div class="now-playing">
|
||||||
{% if scrobble.media_obj.album.cover_image %}
|
{% if scrobble.media_obj.album.cover_image %}
|
||||||
<div style="float:left;padding-right:5px;"><img src="{{scrobble.track.album.cover_image.url}}" width=75 height=75 style="border:1px solid black; " /></div>
|
<div style="float:left;padding-right:5px;"><img src="{{scrobble.media_obj.album.cover_image.url}}" /></div>
|
||||||
|
{% endif %}
|
||||||
|
{% if scrobble.media_obj.cover %}
|
||||||
|
<div style="float:left;padding-right:5px;"><img src="{{scrobble.media_obj.cover.url}}" /></div>
|
||||||
|
{% endif %}
|
||||||
|
{% if scrobble.media_obj.hltb_cover %}
|
||||||
|
<div style="float:left;padding-right:5px;"><img src="{{scrobble.media_obj.hltb_cover.url}}" /></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p><a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj.title}}</a></p>
|
<p><a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj.title}}</a></p>
|
||||||
{% if scrobble.media_obj.subtitle %}
|
{% if scrobble.media_obj.subtitle %}
|
||||||
<p><em><a href="{{scrobble.media_obj.subtitle.get_absolute_url}}">{{scrobble.media_obj.subtitle}}</a></em></p>
|
<p><em><a href="{{scrobble.media_obj.subtitle.get_absolute_url}}">{{scrobble.media_obj.subtitle}}</a></em></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br/>
|
|
||||||
<p><small>{{scrobble.timestamp|naturaltime}} from {{scrobble.source}}</small></p>
|
<p><small>{{scrobble.timestamp|naturaltime}} from {{scrobble.source}}</small></p>
|
||||||
<div class="progress-bar" style="margin-right:5px;">
|
<div class="progress-bar" style="margin-right:5px;">
|
||||||
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
|
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
|
||||||
@ -259,6 +266,7 @@
|
|||||||
<a href="{% url "scrobbles:cancel" scrobble.uuid %}">Cancel</a>
|
<a href="{% url "scrobbles:cancel" scrobble.uuid %}">Cancel</a>
|
||||||
<a class="right" href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a>
|
<a class="right" href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a>
|
||||||
</p>
|
</p>
|
||||||
|
{% if not forloop.last %}<hr/>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
{% block title %}Long Plays{% endblock %}
|
{% block title %}Long Plays{% endblock %}
|
||||||
|
|
||||||
|
{% block head_extra %}
|
||||||
|
<style>
|
||||||
|
dl { width: 260px; float:left; margin-right: 10px; }
|
||||||
|
dt a { color:white; text-decoration: none; font-size:smaller; }
|
||||||
|
img { height:200px; width: 250px; object-fit: cover; }
|
||||||
|
dd .right { float:right; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block lists %}
|
{% block lists %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
@ -10,14 +19,22 @@
|
|||||||
<div>
|
<div>
|
||||||
{% for media in in_progress %}
|
{% for media in in_progress %}
|
||||||
{% if media.hltb_cover %}
|
{% if media.hltb_cover %}
|
||||||
<dl style="width: 210px; float: left; margin-right:10px;">
|
<dl>
|
||||||
<dd><a href="{{media.get_absolute_url}}"><img src="{{media.hltb_cover.url}}" width=200 /></a></dd>
|
|
||||||
<dt><a href="{{media.get_absolute_url}}">{{media.title}}</a></dt>
|
<dt><a href="{{media.get_absolute_url}}">{{media.title}}</a></dt>
|
||||||
|
<dd><a href="{{media.get_absolute_url}}"><img src="{{media.hltb_cover.url}}" width=200 height=200 /></a></dd>
|
||||||
|
<dd>
|
||||||
|
<a type="button" class="btn btn-sm btn-primary" href="{{media.get_start_url}}">Resume</a>
|
||||||
|
<a type="button" class="right btn btn-sm " href="{{media.get_longplay_finish_url}}">Finish</a>
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
{% elif media.cover %}
|
{% elif media.cover %}
|
||||||
<dl style="width: 210px; float: left; margin-right:10px;">
|
<dl>
|
||||||
<dd><a href="{{media.get_absolute_url}}"><img src="{{media.cover.url}}" width=200 /></a></dd>
|
|
||||||
<dt><a href="{{media.get_absolute_url}}">{{media.title}}</a></dt>
|
<dt><a href="{{media.get_absolute_url}}">{{media.title}}</a></dt>
|
||||||
|
<dd><a href="{{media.get_absolute_url}}"><img src="{{media.cover.url}}" style="width: 200px; height: 200px; object-fit:cover; " /></a></dd>
|
||||||
|
<dd>
|
||||||
|
<a type="button" class="btn btn-sm btn-primary" href="{{media.get_start_url}}">Resume</a>
|
||||||
|
<a type="button" class="right btn btn-sm " href="{{media.get_longplay_finish_url}}">Finish</a>
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -45,17 +62,18 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<hr/>
|
||||||
<h2>Completed</h2>
|
<h2>Completed</h2>
|
||||||
{% if view == 'grid' %}
|
{% if view == 'grid' %}
|
||||||
<div>
|
<div>
|
||||||
{% for media in completed %}
|
{% for media in completed %}
|
||||||
{% if media.hltb_cover %}
|
{% if media.hltb_cover %}
|
||||||
<dl style="width: 210px; float: left; margin-right:10px;">
|
<dl>
|
||||||
<dd><a href="{{media.get_absolute_url}}"><img src="{{media.hltb_cover.url}}" width=200 /></a></dd>
|
<dd><a href="{{media.get_absolute_url}}"><img src="{{media.hltb_cover.url}}" width=200 /></a></dd>
|
||||||
<dt><a href="{{media.get_absolute_url}}">{{media.title}}</a></dt>
|
<dt><a href="{{media.get_absolute_url}}">{{media.title}}</a></dt>
|
||||||
</dl>
|
</dl>
|
||||||
{% elif media.cover %}
|
{% elif media.cover %}
|
||||||
<dl style="width: 210px; float: left; margin-right:10px;">
|
<dl>
|
||||||
<dd><a href="{{media.get_absolute_url}}"><img src="{{media.cover.url}}" width=200 /></a></dd>
|
<dd><a href="{{media.get_absolute_url}}"><img src="{{media.cover.url}}" width=200 /></a></dd>
|
||||||
<dt><a href="{{media.get_absolute_url}}">{{media.title}}</a></dt>
|
<dt><a href="{{media.get_absolute_url}}">{{media.title}}</a></dt>
|
||||||
</dl>
|
</dl>
|
||||||
|
|||||||
@ -5,16 +5,24 @@
|
|||||||
|
|
||||||
{% block title %}{{object.title}}{% endblock %}
|
{% block title %}{{object.title}}{% endblock %}
|
||||||
|
|
||||||
|
{% block head_extra %}
|
||||||
|
<style>
|
||||||
|
.cover img { width: 250px; }
|
||||||
|
.cover { float: left; width:252px; padding:0; border: 1px solid #ccc; }
|
||||||
|
.summary {
|
||||||
|
float:left; width:600px; margin-left:10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block lists %}
|
{% block lists %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
{% if object.hltb_cover%}
|
{% if object.hltb_cover%}
|
||||||
<p style="float:left; width:202px; padding:0; border: 1px solid #ccc">
|
<div class="cover"><img src="{{object.hltb_cover.url}}" /></div>
|
||||||
<img src="{{object.hltb_cover.url}}" width=200 />
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div style="float:left; width:600px; margin-left:10px; ">
|
<div class="summary">
|
||||||
{% if object.summary %}
|
{% if object.summary %}
|
||||||
<p>{{object.summary|safe|linebreaks|truncatewords:160}}</p>
|
<p>{{object.summary|safe|linebreaks|truncatewords:160}}</p>
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|||||||
Reference in New Issue
Block a user