coderedcorp / coderedcms

Wagtail + CodeRed Extensions enabling rapid development of marketing-focused websites.
https://www.coderedcorp.com/cms
Other
668 stars 137 forks source link

approach for combinatorial model field? #632

Closed jvolkening closed 2 months ago

jvolkening commented 2 months ago

Thanks for providing these extensions -- I started with Wagtail and then found CRX and it has been exactly what I was looking for to migrate an existing low-traffic non-profit website created by others in WordPress.

I've been able to do almost everything I planned, with one remaining exception. I would like to keep track of donors in such a way that they can be displayed in different page templates, using different filters for year and giving tier. I have a Donor snippet class in which I originally had the giving tier as a model field (based on IntegerChoices submodel) and the giving year specified using tags:

@register_snippet
class Donor(ClusterableModel):
    """Class representing a single donor."""

    class DonorLevel(models.IntegerChoices):
        CONCERT = 9, 'Concert ($2500+)'
        GUEST_ARTIST = 7, 'Guest Artist ($1000-$2499)'
        CONDUCTOR_CIRCLE = 5, 'Conductor Circle ($500-$999)'
        MUSICIAN = 3, 'Musician ($150-$499)'
        SENIOR_SPOTLIGHT = 2, 'Senior Spotlight'
        GENERAL = 1, 'General ($5-$149)'

    class Meta:
        verbose_name = "Donor"
        verbose_name_plural = "Donors"

    name = models.CharField(
        max_length=255,
        verbose_name="Full Name",
    )
    short_name = models.CharField(
        max_length=64,
        verbose_name="Short Name",
        blank = True,
    )
    image = models.ForeignKey(
        get_image_model_string(),
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
        verbose_name="Logo",
    )
    level = models.IntegerField(
        choices = DonorLevel.choices,
        default = DonorLevel.GENERAL,
        verbose_name="Donor Level",
    )
    is_active = models.BooleanField(
        default = True,
        verbose_name="Active",
    )
    url = models.URLField(
        max_length=255,
        verbose_name="URL",
        blank = True,
    )
    tags = TaggableManager(through=DonorTag, blank=True)
    levels = models.ManyToManyField(DonorTier)

    panels = [
        MultiFieldPanel(
            heading="Donor",
            children=[
                FieldPanel("name"),
                FieldPanel("short_name"),
                FieldPanel("image"),
                FieldPanel("level"),
                FieldPanel("url"),
                FieldPanel("tags"),
                FieldPanel("is_active"),
            ],
        ),
    ]

    def __str__(self):
        return self.name

However, in looking forward I realize that a Donor's giving tier may change from year to year. Therefore, the giving tier and year need to be some sort of combinatorial field. I would also like to be able to keep track of and display giving history, so I don't want to simply update the giving tier each year if a donor gives in multiple years. This needs to be manageable through the admin interface. Being new to Django/Wagtail/CRX, the best I could think of was to create a separate snippet model to track this:

@register_snippet
class DonorTier(models.Model):

    def current_year():
        return datetime.date.today().year

    def max_value_current_year(value):
        return MaxValueValidator(current_year()+1)(value)

    class DonorLevel(models.IntegerChoices):
        CONCERT = 9, 'Concert ($2500+)'
        GUEST_ARTIST = 7, 'Guest Artist ($1000-$2499)'
        CONDUCTOR_CIRCLE = 5, 'Conductor Circle ($500-$999)'
        MUSICIAN = 3, 'Musician ($150-$499)'
        SENIOR_SPOTLIGHT = 2, 'Senior Spotlight'
        GENERAL = 1, 'General ($5-$149)'

    class Meta:
        verbose_name = "Donor Year-Tier"
        verbose_name_plural = "Donor Year-Tiers"

    year = models.PositiveIntegerField(
        default=current_year(), validators=[MinValueValidator(2010), max_value_current_year])
    level = models.IntegerField(
        choices = DonorLevel.choices,
        default = DonorLevel.GENERAL,
        verbose_name="Donor Level",
    )
    panels = [
        FieldPanel("year"),
        FieldPanel("level"),
    ]

    def __str__(self):
        return '-'.join([str(self.year), self.get_level_display()])

and then add this as a field to Donor:

    levels = models.ManyToManyField(DonorTier)

This seems to work to an extent, but it means creating a new DonorTier snippet instance for each combination of year and tier, as well as having an additional 'Donor Year-Tiers' area cluttering up the Snippets admin panel just to store this information. Is there a better way to do this, or at least a way that is more friendly for the site admins who will be managing this information?

Many thanks for any tips or guidance.

jvolkening commented 2 months ago

I received some help on the Wagtail Slack and found a solution using inline models that works well enough for our needs.