diff --git a/tubesync/sync/fields.py b/tubesync/sync/fields.py
index 31889024..a18518a1 100644
--- a/tubesync/sync/fields.py
+++ b/tubesync/sync/fields.py
@@ -1,6 +1,8 @@
-from django.forms import MultipleChoiceField, CheckboxSelectMultiple, Field, TypedMultipleChoiceField
-from django.db import models
-from typing import Any, Optional, Dict
+from collections import namedtuple
+from functools import lru_cache
+from typing import Any, Dict
+from django import forms
+from django.db import connection, models
from django.utils.translation import gettext_lazy as _
@@ -22,103 +24,174 @@ class SponsorBlock_Category(models.TextChoices):
MUSIC_OFFTOPIC = 'music_offtopic', _( 'Non-Music Section' )
+CommaSepChoice = namedtuple(
+ 'CommaSepChoice', [
+ 'allow_all',
+ 'all_choice',
+ 'all_label',
+ 'possible_choices',
+ 'selected_choices',
+ 'separator',
+ ],
+ defaults = (
+ False,
+ None,
+ 'All',
+ list(),
+ list(),
+ ',',
+ ),
+)
+
# this is a form field!
-class CustomCheckboxSelectMultiple(CheckboxSelectMultiple):
+class CustomCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
template_name = 'widgets/checkbox_select.html'
option_template_name = 'widgets/checkbox_option.html'
+ # perhaps set the 'selected' attribute too?
+ # checked_attribute = {'checked': True, 'selected': True}
def get_context(self, name: str, value: Any, attrs) -> Dict[str, Any]:
- ctx = super().get_context(name, value, attrs)['widget']
- ctx["multipleChoiceProperties"] = []
- for _group, options, _index in ctx["optgroups"]:
- for option in options:
- if not isinstance(value,str) and not isinstance(value,list) and ( option["value"] in value.selected_choices or ( value.allow_all and value.all_choice in value.selected_choices ) ):
- checked = True
- else:
- checked = False
+ data = value
+ select_all = False
+ if isinstance(data, CommaSepChoice):
+ select_all = (data.allow_all and data.all_choice in data.selected_choices)
+ value = list(data.selected_choices)
+ context = super().get_context(name, value, attrs)
+ widget = context['widget']
+ options = widget['optgroups']
+ # This is a new key in widget
+ widget['multipleChoiceProperties'] = list()
+ for _group, single_option_list, _index in options:
+ for option in single_option_list:
+ option['selected'] |= select_all
+ widget['multipleChoiceProperties'].append(option)
- ctx["multipleChoiceProperties"].append({
- "template_name": option["template_name"],
- "type": option["type"],
- "value": option["value"],
- "label": option["label"],
- "name": option["name"],
- "checked": checked})
+ return { 'widget': widget }
- return { 'widget': ctx }
# this is a database field!
-class CommaSepChoiceField(models.Field):
- "Implements comma-separated storage of lists"
+class CommaSepChoiceField(models.CharField):
+ '''
+ Implements comma-separated storage of lists
+ '''
- # If 'text' isn't correct add the vendor override here.
- _DB_TYPES = {}
+ form_class = forms.MultipleChoiceField
+ widget = CustomCheckboxSelectMultiple
+ from common.logger import log
def __init__(self, *args, separator=",", possible_choices=(("","")), all_choice="", all_label="All", allow_all=False, **kwargs):
- super().__init__(*args, **kwargs)
+ kwargs.setdefault('max_length', 128)
self.separator = str(separator)
- self.possible_choices = possible_choices
- self.selected_choices = []
+ self.possible_choices = possible_choices or choices
+ self.selected_choices = list()
self.allow_all = allow_all
self.all_label = all_label
self.all_choice = all_choice
+ self.choices = self.get_all_choices()
+ super().__init__(*args, **kwargs)
+ self.validators.clear()
+
+ # Override these functions to prevent unwanted behaviors
+ def to_python(self, value):
+ return value
+
+ def get_internal_type(self):
+ return super().get_internal_type()
+
+
+ # standard functions for this class
def deconstruct(self):
+ # set it back to the default for models.Field
+ # this way it is never in the returned values
+ self.choices = None
name, path, args, kwargs = super().deconstruct()
+ self.choices = self.get_all_choices()
if ',' != self.separator:
kwargs['separator'] = self.separator
kwargs['possible_choices'] = self.possible_choices
- return name, path, args, kwargs
-
- def db_type(self, connection):
- value = self._DB_TYPES.get(connection.vendor, None)
- return value if value is not None else 'text'
-
- def get_my_choices(self):
- choiceArray = []
- if self.possible_choices is None:
- return choiceArray
if self.allow_all:
- choiceArray.append((self.all_choice, _(self.all_label)))
-
- for t in self.possible_choices:
- choiceArray.append(t)
-
- return choiceArray
+ kwargs['allow_all'] = self.allow_all
+ if self.all_choice:
+ kwargs['all_choice'] = self.all_choice
+ if 'All' != self.all_label:
+ kwargs['all_label'] = self.all_label
+ return name, path, args, kwargs
def formfield(self, **kwargs):
# This is a fairly standard way to set up some defaults
# while letting the caller override them.
- defaults = {'form_class': MultipleChoiceField,
- 'choices': self.get_my_choices,
- 'widget': CustomCheckboxSelectMultiple,
- 'label': '',
- 'required': False}
+ defaults = {
+ 'form_class': self.form_class,
+ # 'choices_form_class': self.form_class,
+ 'widget': self.widget,
+ # use a callable for choices
+ 'choices': self.get_all_choices,
+ 'label': '',
+ 'required': False,
+ }
+ # Keep the part from CharField we want,
+ # then call Field to skip the 'max_length' entry.
+ db_empty_string_as_null = connection.features.interprets_empty_strings_as_nulls
+ if self.null and not db_empty_string_as_null:
+ defaults['empty_value'] = None
defaults.update(kwargs)
- return super().formfield(**defaults)
+ return models.Field.formfield(self, **defaults)
+ # This is a more compact way to do the same thing
+ # return super().formfield(**{
+ # 'form_class': self.form_class,
+ # **kwargs,
+ # })
- def from_db_value(self, value, expr, conn):
- if 0 == len(value) or value is None:
- self.selected_choices = []
- else:
- self.selected_choices = value.split(self.separator)
+ @lru_cache(maxsize=10)
+ def from_db_value(self, value, expression, connection):
+ '''
+ Create a data structure to be used in Python code.
- return self
+ This is called quite often with the same input,
+ because the database value doesn't change often.
+ So, it's being cached to prevent excessive logging.
+ '''
+ self.log.debug(f'fdbv:1: {type(value)} {repr(value)}')
+ if isinstance(value, str) and len(value) > 0:
+ value = value.split(self.separator)
+ if not isinstance(value, list):
+ value = list()
+ self.selected_choices = value
+ args_dict = {key: self.__dict__[key] for key in CommaSepChoice._fields}
+ return CommaSepChoice(**args_dict)
def get_prep_value(self, value):
- if value is None:
- return ""
- if not isinstance(value,list):
- return ""
+ '''
+ Create a value to be stored in the database.
+ '''
+ data = value
+ if not isinstance(data, CommaSepChoice):
+ # The data was lost; we can regenerate it.
+ args_dict = {key: self.__dict__[key] for key in CommaSepChoice._fields}
+ args_dict['selected_choices'] = list(value)
+ data = CommaSepChoice(**args_dict)
+ value = data.selected_choices
+ s_value = super().get_prep_value(value)
+ if set(s_value) != set(value):
+ self.log.warn(f'CommaSepChoiceField:get_prep_value: values did not match. '
+ f'CommaSepChoiceField({value}) versus CharField({s_value})')
+ if not isinstance(value, list):
+ return ''
+ if data.all_choice in value:
+ return data.all_choice
+ return data.separator.join(value)
- if self.all_choice not in value:
- return self.separator.join(value)
- else:
- return self.all_choice
+ # extra functions not used by any parent classes
+ def get_all_choices(self):
+ choice_list = list()
+ if self.possible_choices is None:
+ return choice_list
+ if self.allow_all:
+ choice_list.append((self.all_choice, _(self.all_label)))
+
+ for choice in self.possible_choices:
+ choice_list.append(choice)
+
+ return choice_list
- def get_text_for_value(self, val):
- fval = [i for i in self.possible_choices if i[0] == val]
- if len(fval) <= 0:
- return []
- else:
- return fval[0][1]
diff --git a/tubesync/sync/migrations/0027_alter_source_sponsorblock_categories.py b/tubesync/sync/migrations/0027_alter_source_sponsorblock_categories.py
index 92fbc98a..c81b8e72 100644
--- a/tubesync/sync/migrations/0027_alter_source_sponsorblock_categories.py
+++ b/tubesync/sync/migrations/0027_alter_source_sponsorblock_categories.py
@@ -14,6 +14,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='source',
name='sponsorblock_categories',
- field=sync.fields.CommaSepChoiceField(default='all', help_text='Select the sponsorblocks you want to enforce', possible_choices=[('sponsor', 'Sponsor'), ('intro', 'Intermission/Intro Animation'), ('outro', 'Endcards/Credits'), ('selfpromo', 'Unpaid/Self Promotion'), ('preview', 'Preview/Recap'), ('filler', 'Filler Tangent'), ('interaction', 'Interaction Reminder'), ('music_offtopic', 'Non-Music Section')], verbose_name=''),
+ field=sync.fields.CommaSepChoiceField(all_choice='all', all_label='(All Categories)', allow_all=True, default='all', help_text='Select the SponsorBlock categories that you wish to be removed from downloaded videos.', max_length=128, possible_choices=[('sponsor', 'Sponsor'), ('intro', 'Intermission/Intro Animation'), ('outro', 'Endcards/Credits'), ('selfpromo', 'Unpaid/Self Promotion'), ('preview', 'Preview/Recap'), ('filler', 'Filler Tangent'), ('interaction', 'Interaction Reminder'), ('music_offtopic', 'Non-Music Section')], verbose_name=''),
),
]
diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py
index 877b62e6..2daeb094 100644
--- a/tubesync/sync/models.py
+++ b/tubesync/sync/models.py
@@ -118,12 +118,13 @@ class Source(models.Model):
sponsorblock_categories = CommaSepChoiceField(
_(''),
+ max_length=128,
possible_choices=SponsorBlock_Category.choices,
all_choice='all',
allow_all=True,
- all_label='(all options)',
+ all_label='(All Categories)',
default='all',
- help_text=_('Select the sponsorblocks you want to enforce')
+ help_text=_('Select the SponsorBlock categories that you wish to be removed from downloaded videos.')
)
embed_metadata = models.BooleanField(
_('embed metadata'),
diff --git a/tubesync/sync/templates/widgets/checkbox_option.html b/tubesync/sync/templates/widgets/checkbox_option.html
index 06a6723e..db32a457 100644
--- a/tubesync/sync/templates/widgets/checkbox_option.html
+++ b/tubesync/sync/templates/widgets/checkbox_option.html
@@ -2,6 +2,6 @@
-->
\ No newline at end of file
+