[scrobbles] Add note taking to webpages
This commit is contained in:
@ -1,3 +1,5 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from scrobbles.models import (
|
from scrobbles.models import (
|
||||||
AudioScrobblerTSVImport,
|
AudioScrobblerTSVImport,
|
||||||
@ -7,10 +9,52 @@ from scrobbles.models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
NOTE_POSITION_RE = re.compile(r"\[start:(\d+)\|end:(\d+)\]$")
|
||||||
|
|
||||||
|
|
||||||
class ScrobbleSerializer(serializers.HyperlinkedModelSerializer):
|
class ScrobbleSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
note = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
note_position = serializers.DictField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Scrobble
|
model = Scrobble
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
extra_kwargs = {
|
||||||
|
"url": {"view_name": "scrobble-detail", "lookup_field": "uuid"},
|
||||||
|
}
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
note = validated_data.pop("note", None)
|
||||||
|
note_position = validated_data.pop("note_position", None)
|
||||||
|
|
||||||
|
instance = super().update(instance, validated_data)
|
||||||
|
|
||||||
|
if note is not None:
|
||||||
|
log = instance.log or {}
|
||||||
|
notes = log.get("notes", [])
|
||||||
|
if isinstance(notes, str):
|
||||||
|
notes = [notes]
|
||||||
|
elif notes is None:
|
||||||
|
notes = []
|
||||||
|
|
||||||
|
if note_position:
|
||||||
|
note = f"{note} [start:{note_position['start']}|end:{note_position['end']}]"
|
||||||
|
|
||||||
|
notes.append(note)
|
||||||
|
log["notes"] = notes
|
||||||
|
instance.log = log
|
||||||
|
instance.save(update_fields=["log"])
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
def parse_note_position(note):
|
||||||
|
match = NOTE_POSITION_RE.search(note)
|
||||||
|
if match:
|
||||||
|
start, end = match.groups()
|
||||||
|
text = NOTE_POSITION_RE.sub("", note)
|
||||||
|
return text, {"start": int(start), "end": int(end)}
|
||||||
|
return note, None
|
||||||
|
|
||||||
|
|
||||||
class KoReaderImportSerializer(serializers.HyperlinkedModelSerializer):
|
class KoReaderImportSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
@ -19,9 +63,7 @@ class KoReaderImportSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class AudioScrobblerTSVImportSerializer(
|
class AudioScrobblerTSVImportSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
serializers.HyperlinkedModelSerializer
|
|
||||||
):
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AudioScrobblerTSVImport
|
model = AudioScrobblerTSVImport
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
from logging import getLogger
|
||||||
|
|
||||||
from rest_framework import permissions, viewsets
|
from rest_framework import permissions, viewsets
|
||||||
from scrobbles.api.serializers import (
|
from scrobbles.api.serializers import (
|
||||||
AudioScrobblerTSVImportSerializer,
|
AudioScrobblerTSVImportSerializer,
|
||||||
@ -12,11 +14,14 @@ from scrobbles.models import (
|
|||||||
LastFmImport,
|
LastFmImport,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ScrobbleViewSet(viewsets.ModelViewSet):
|
class ScrobbleViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Scrobble.objects.all().order_by("-timestamp")
|
queryset = Scrobble.objects.all().order_by("-timestamp")
|
||||||
serializer_class = ScrobbleSerializer
|
serializer_class = ScrobbleSerializer
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
lookup_field = "uuid"
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().filter(user=self.request.user)
|
return super().get_queryset().filter(user=self.request.user)
|
||||||
|
|||||||
@ -16,6 +16,26 @@ class WebPageListView(ScrobbleableListView):
|
|||||||
class WebPageDetailView(ScrobbleableDetailView):
|
class WebPageDetailView(ScrobbleableDetailView):
|
||||||
model = WebPage
|
model = WebPage
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
latest = self.object.scrobbles(self.request.user).first()
|
||||||
|
if latest:
|
||||||
|
context["latest_scrobble_uuid"] = str(latest.uuid)
|
||||||
|
context["latest_scrobble_notes"] = (
|
||||||
|
latest.log.get("notes", []) if latest.log else []
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"DEBUG: Found scrobble {latest.uuid} for user {self.request.user}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"DEBUG: No scrobble found for user {self.request.user} on webpage {self.object.uuid}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"DEBUG: User not authenticated")
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class WebPageReadView(
|
class WebPageReadView(
|
||||||
LoginRequiredMixin, generic.edit.FormView, generic.DetailView
|
LoginRequiredMixin, generic.edit.FormView, generic.DetailView
|
||||||
|
|||||||
@ -16,12 +16,88 @@
|
|||||||
<script src="{% static 'js/recogito.min.js' %}"></script>
|
<script src="{% static 'js/recogito.min.js' %}"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
(function() {
|
(function() {
|
||||||
|
var latestScrobbleUuid = "{{ latest_scrobble_uuid|default:'' }}";
|
||||||
|
console.log('DEBUG: latestScrobbleUuid =', latestScrobbleUuid);
|
||||||
|
if (!latestScrobbleUuid) {
|
||||||
|
console.log('No latest scrobble UUID available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var r = Recogito.init({
|
var r = Recogito.init({
|
||||||
content: document.getElementById('article') // ID or DOM element
|
content: document.getElementById('article')
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add an event handler
|
var notePositionRe = /\[start:(\d+)\|end:(\d+)\]$/;
|
||||||
r.on('createAnnotation', function(annotation) { /** **/ });
|
|
||||||
|
function parseNote(noteStr) {
|
||||||
|
var match = noteStr.match(notePositionRe);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
text: noteStr.replace(notePositionRe, ''),
|
||||||
|
position: { start: parseInt(match[1]), end: parseInt(match[2]) }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { text: noteStr, position: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingNotes = [
|
||||||
|
{% for note in latest_scrobble_notes %}
|
||||||
|
(function() {
|
||||||
|
var parsed = parseNote("{{ note|escapejs }}");
|
||||||
|
if (parsed.position) {
|
||||||
|
return {
|
||||||
|
"@context": "http://www.w3.org/ns/anno.jsonld",
|
||||||
|
"@type": "Annotation",
|
||||||
|
"body": [{"@type": "TextualBody", "value": parsed.text}],
|
||||||
|
"target": {"selector": [{"type": "TextPositionSelector", "start": parsed.position.start, "end": parsed.position.end}]}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})(),
|
||||||
|
{% endfor %}
|
||||||
|
].filter(function(n) { return n !== null; });
|
||||||
|
|
||||||
|
if (existingNotes.length > 0) {
|
||||||
|
r.setAnnotations(existingNotes);
|
||||||
|
}
|
||||||
|
|
||||||
|
r.on('createAnnotation', function(annotation) {
|
||||||
|
var note = annotation.body && annotation.body[0] ? annotation.body[0].value : null;
|
||||||
|
if (!note) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var position = null;
|
||||||
|
if (annotation.target && annotation.target.selector) {
|
||||||
|
var selector = annotation.target.selector;
|
||||||
|
for (var i = 0; i < selector.length; i++) {
|
||||||
|
if (selector[i].type === 'TextPositionSelector') {
|
||||||
|
position = { start: selector[i].start, end: selector[i].end };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Sending fetch to:', '/api/v1/scrobbles/' + latestScrobbleUuid + '/');
|
||||||
|
console.log('CSRF token:', '{{ csrf_token }}');
|
||||||
|
fetch('/api/v1/scrobbles/' + latestScrobbleUuid + '/', {
|
||||||
|
method: 'PATCH',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ note: note, note_position: position })
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Failed to save note:', response.status, response.statusText);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error saving note:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user