[boardgames] Add ability to add new variants to form
This commit is contained in:
15
PROJECT.org
15
PROJECT.org
@ -88,7 +88,7 @@ fetching and simple saving.
|
|||||||
*** Metadata sources
|
*** Metadata sources
|
||||||
**** Scraper
|
**** Scraper
|
||||||
|
|
||||||
* Backlog [0/24] :vrobbler:project:personal:
|
* Backlog [1/26] :vrobbler:project:personal:
|
||||||
** TODO [#C] After transition to linux add curl_cffi as webpage scrapper again :webpages:metadata:
|
** TODO [#C] After transition to linux add curl_cffi as webpage scrapper again :webpages:metadata:
|
||||||
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles:
|
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
@ -619,6 +619,19 @@ The Edit log form should have from top to bottom:
|
|||||||
- Expansion ids (which should a multi-select widget of expansions for this game)
|
- Expansion ids (which should a multi-select widget of expansions for this game)
|
||||||
- Location (which should be a drop down of BoardGameLocations for this user)
|
- Location (which should be a drop down of BoardGameLocations for this user)
|
||||||
|
|
||||||
|
** TODO Exclude some board games from auto-expansion imports :boardgames:
|
||||||
|
|
||||||
|
*** Description
|
||||||
|
|
||||||
|
Some board games, especially trading card games, have silly amounts of expansions.
|
||||||
|
We should have a setting SKIP_AUTO_EXPANSION_DOWNLOAD that is a list of BGG ids of
|
||||||
|
games where expansions should not be download automatically. This exclusion should also auto-include
|
||||||
|
any games with "Collectible Card Games" in it's family.
|
||||||
|
|
||||||
|
** DONE Should be able to add new variants to board games via the log data form :boardgames:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 5ed0dd25-3026-3da8-dc5c-f2a75751af9a
|
||||||
|
:END:
|
||||||
* Version 59.2 [1/1]
|
* Version 59.2 [1/1]
|
||||||
** DONE Fix test failure in discgolf app :discgolf:tests:
|
** DONE Fix test failure in discgolf app :discgolf:tests:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
|
|||||||
@ -93,6 +93,7 @@ class BoardGameLogData(BaseLogData, LongPlayLogData):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def override_fields(cls) -> dict:
|
def override_fields(cls) -> dict:
|
||||||
|
from boardgames.widgets import VariantSelectWidget
|
||||||
from scrobbles.forms import NotesDictField
|
from scrobbles.forms import NotesDictField
|
||||||
|
|
||||||
fields = {}
|
fields = {}
|
||||||
@ -110,7 +111,7 @@ class BoardGameLogData(BaseLogData, LongPlayLogData):
|
|||||||
"variant_ids": forms.ModelMultipleChoiceField(
|
"variant_ids": forms.ModelMultipleChoiceField(
|
||||||
queryset=BoardGameVariant.objects.all(),
|
queryset=BoardGameVariant.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.SelectMultiple(attrs={"size": 5}),
|
widget=VariantSelectWidget(attrs={"size": 5}),
|
||||||
),
|
),
|
||||||
"expansion_ids": forms.ModelMultipleChoiceField(
|
"expansion_ids": forms.ModelMultipleChoiceField(
|
||||||
queryset=BoardGame.objects.filter(
|
queryset=BoardGame.objects.filter(
|
||||||
|
|||||||
@ -0,0 +1,97 @@
|
|||||||
|
<select name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
|
||||||
|
{% for group_name, group_choices, group_index in widget.optgroups %}
|
||||||
|
{% for option in group_choices %}
|
||||||
|
{% include option.template_name with widget=option %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" data-bs-toggle="modal" data-bs-target="#addVariantModal">
|
||||||
|
+ Add variant
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="modal fade" id="addVariantModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Add Variant</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="newVariantName" class="form-label">Name</label>
|
||||||
|
<input type="text" class="form-control" id="newVariantName" placeholder="e.g. Map A">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="newVariantDescription" class="form-label">Description (optional)</label>
|
||||||
|
<input type="text" class="form-control" id="newVariantDescription">
|
||||||
|
</div>
|
||||||
|
<p class="text-danger d-none" id="variantError"></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="saveVariantBtn">Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var select = document.getElementById('{{ widget.attrs.id }}');
|
||||||
|
if (!select) return;
|
||||||
|
|
||||||
|
var saveBtn = document.getElementById('saveVariantBtn');
|
||||||
|
if (!saveBtn) return;
|
||||||
|
|
||||||
|
var modalEl = document.getElementById('addVariantModal');
|
||||||
|
var nameInput = document.getElementById('newVariantName');
|
||||||
|
var descInput = document.getElementById('newVariantDescription');
|
||||||
|
var errorEl = document.getElementById('variantError');
|
||||||
|
|
||||||
|
saveBtn.addEventListener('click', function() {
|
||||||
|
var name = nameInput.value.trim();
|
||||||
|
if (!name) return;
|
||||||
|
|
||||||
|
var boardGameId = select.getAttribute('data-board-game-id');
|
||||||
|
var ajaxUrl = select.getAttribute('data-ajax-url');
|
||||||
|
var csrfToken = document.querySelector('[name=csrfmiddlewaretoken]');
|
||||||
|
if (!csrfToken) return;
|
||||||
|
|
||||||
|
errorEl.classList.add('d-none');
|
||||||
|
|
||||||
|
fetch(ajaxUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRFToken': csrfToken.value,
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
name: name,
|
||||||
|
description: descInput.value.trim(),
|
||||||
|
board_game_id: boardGameId,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.error) {
|
||||||
|
errorEl.textContent = data.error;
|
||||||
|
errorEl.classList.remove('d-none');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var opt = document.createElement('option');
|
||||||
|
opt.value = data.id;
|
||||||
|
opt.textContent = data.name;
|
||||||
|
opt.selected = true;
|
||||||
|
select.appendChild(opt);
|
||||||
|
var modal = bootstrap.Modal.getInstance(modalEl);
|
||||||
|
if (modal) modal.hide();
|
||||||
|
nameInput.value = '';
|
||||||
|
descInput.value = '';
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
errorEl.textContent = 'Failed to create variant';
|
||||||
|
errorEl.classList.remove('d-none');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@ -20,4 +20,9 @@ urlpatterns = [
|
|||||||
views.BoardGamePublisherDetailView.as_view(),
|
views.BoardGamePublisherDetailView.as_view(),
|
||||||
name="publisher_detail",
|
name="publisher_detail",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"variants/ajax-create/",
|
||||||
|
views.ajax_create_variant,
|
||||||
|
name="ajax-create-variant",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from django.http import JsonResponse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
from boardgames.models import BoardGame, BoardGameDesigner, BoardGamePublisher
|
from boardgames.models import BoardGame, BoardGameDesigner, BoardGamePublisher
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
from scrobbles.views import (
|
from scrobbles.views import (
|
||||||
@ -10,6 +13,38 @@ from scrobbles.views import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def ajax_create_variant(request):
|
||||||
|
name = request.POST.get("name", "").strip()
|
||||||
|
board_game_id = request.POST.get("board_game_id")
|
||||||
|
description = request.POST.get("description", "").strip()
|
||||||
|
|
||||||
|
if not name or not board_game_id:
|
||||||
|
return JsonResponse({"error": "Name and board game are required"}, status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
board_game_id = int(board_game_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return JsonResponse({"error": "Invalid board game"}, status=400)
|
||||||
|
|
||||||
|
from boardgames.models import BoardGameVariant
|
||||||
|
|
||||||
|
variant = BoardGameVariant.objects.filter(
|
||||||
|
name__iexact=name,
|
||||||
|
board_game_id=board_game_id,
|
||||||
|
).first()
|
||||||
|
if variant:
|
||||||
|
return JsonResponse({"id": variant.id, "name": variant.name})
|
||||||
|
|
||||||
|
variant = BoardGameVariant.objects.create(
|
||||||
|
name=name,
|
||||||
|
board_game_id=board_game_id,
|
||||||
|
description=description or None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return JsonResponse({"id": variant.id, "name": variant.name})
|
||||||
|
|
||||||
|
|
||||||
class BoardGameListView(ScrobbleableListView):
|
class BoardGameListView(ScrobbleableListView):
|
||||||
model = BoardGame
|
model = BoardGame
|
||||||
|
|
||||||
|
|||||||
9
vrobbler/apps/boardgames/widgets.py
Normal file
9
vrobbler/apps/boardgames/widgets.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
|
||||||
|
class VariantSelectWidget(forms.SelectMultiple):
|
||||||
|
template_name = "boardgames/widgets/variant_select.html"
|
||||||
|
|
||||||
|
def get_context(self, name, value, attrs):
|
||||||
|
context = super().get_context(name, value, attrs)
|
||||||
|
return context
|
||||||
@ -33,7 +33,7 @@ from django.http import (
|
|||||||
JsonResponse,
|
JsonResponse,
|
||||||
)
|
)
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.dateformat import DateFormat
|
from django.utils.dateformat import DateFormat
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
@ -1235,15 +1235,28 @@ class ScrobbleDetailView(DetailView):
|
|||||||
def get_form_class(self):
|
def get_form_class(self):
|
||||||
return self.object.media_obj.logdata_cls().form()
|
return self.object.media_obj.logdata_cls().form()
|
||||||
|
|
||||||
def _update_expansion_ids_queryset(self, form):
|
def _update_board_game_widgets(self, form):
|
||||||
from boardgames.models import BoardGame
|
from boardgames.models import BoardGame
|
||||||
|
|
||||||
if isinstance(self.object.media_obj, BoardGame) and "expansion_ids" in form.fields:
|
if not isinstance(self.object.media_obj, BoardGame):
|
||||||
|
return
|
||||||
|
|
||||||
|
if "expansion_ids" in form.fields:
|
||||||
expansions = BoardGame.objects.filter(
|
expansions = BoardGame.objects.filter(
|
||||||
expansion_for_boardgame=self.object.media_obj
|
expansion_for_boardgame=self.object.media_obj
|
||||||
)
|
)
|
||||||
form.fields["expansion_ids"].queryset = expansions
|
form.fields["expansion_ids"].queryset = expansions
|
||||||
|
|
||||||
|
if "variant_ids" in form.fields:
|
||||||
|
form.fields["variant_ids"].widget.attrs["data-board-game-id"] = (
|
||||||
|
self.object.media_obj.id
|
||||||
|
)
|
||||||
|
form.fields["variant_ids"].widget.attrs["data-ajax-url"] = (
|
||||||
|
self.request.build_absolute_uri(
|
||||||
|
reverse("boardgames:ajax-create-variant")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def get_form(self):
|
def get_form(self):
|
||||||
FormClass = self.get_form_class()
|
FormClass = self.get_form_class()
|
||||||
|
|
||||||
@ -1255,14 +1268,14 @@ class ScrobbleDetailView(DetailView):
|
|||||||
log["notes"] = self.object.logdata.notes_as_str(separator="\n")
|
log["notes"] = self.object.logdata.notes_as_str(separator="\n")
|
||||||
|
|
||||||
form = FormClass(initial=log)
|
form = FormClass(initial=log)
|
||||||
self._update_expansion_ids_queryset(form)
|
self._update_board_game_widgets(form)
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
FormClass = self.get_form_class()
|
FormClass = self.get_form_class()
|
||||||
form = FormClass(request.POST)
|
form = FormClass(request.POST)
|
||||||
self._update_expansion_ids_queryset(form)
|
self._update_board_game_widgets(form)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
data = form.cleaned_data.copy()
|
data = form.cleaned_data.copy()
|
||||||
|
|||||||
Reference in New Issue
Block a user