+from django.db.models import Q
from django.contrib import admin
from django import forms
+from django.utils.timezone import now
from admin_ordering.admin import OrderableAdmin
from modeltranslation.admin import TranslationAdmin
+from wolnelektury.utils import YesNoFilter
from . import models
+
admin.site.register(models.Campaign)
+class IsCurrentFilter(YesNoFilter):
+ title = 'Aktualny'
+ parameter_name = 'current'
+
+ @property
+ def q(self):
+ n = now()
+ return ~(Q(since__gt=n) | Q(until__lt=n) | Q(campaign__start__gt=n) | Q(campaign__end__lt=n))
+
+
class BannerAdmin(TranslationAdmin):
list_display = [
'place', 'text',
- 'text_color', 'background_color',
- 'priority', 'since', 'until',
+ 'campaign',
+ 'since', 'until',
'show_members', 'staff_preview', 'only_authenticated']
+ list_filter = [
+ 'campaign',
+ IsCurrentFilter,
+ ]
+ autocomplete_fields = ['books']
admin.site.register(models.Banner, BannerAdmin)
--- /dev/null
+# Generated by Django 4.0.8 on 2026-03-31 11:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('annoy', '0019_campaign_banner_books_alter_banner_place_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='campaign',
+ name='end',
+ field=models.DateTimeField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='campaign',
+ name='landing',
+ field=models.CharField(blank=True, max_length=1024),
+ ),
+ migrations.AddField(
+ model_name='campaign',
+ name='priority',
+ field=models.PositiveSmallIntegerField(default=0, help_text='Kampanie z wyższym priorytetem mają pierwszeństwo.', verbose_name='priorytet'),
+ ),
+ migrations.AddField(
+ model_name='campaign',
+ name='progress',
+ field=models.IntegerField(blank=True, null=True, verbose_name='postęp'),
+ ),
+ migrations.AddField(
+ model_name='campaign',
+ name='start',
+ field=models.DateTimeField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='campaign',
+ name='target',
+ field=models.IntegerField(blank=True, null=True, verbose_name='cel'),
+ ),
+ migrations.AlterField(
+ model_name='banner',
+ name='place',
+ field=models.SlugField(choices=[('top', 'U góry wszystkich stron'), ('book-page', 'Strona książki'), ('book-page-center', 'Strona książki, środek'), ('book-text-intermission', 'Przerwa w treści książki'), ('book-fragment-list', 'Obok listy fragmentów książki'), ('blackout', 'Blackout'), ('crisis', 'Kryzysowa'), ('seasonal', 'Sezonowa')], verbose_name='miejsce'),
+ ),
+ migrations.AlterField(
+ model_name='campaign',
+ name='name',
+ field=models.CharField(help_text='Wewnętrzna nazwa kampanii', max_length=255),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2026-03-31 11:10
+
+from django.db import migrations
+
+def data_campaign(apps, schema_editor):
+ Banner = apps.get_model('annoy', 'Banner')
+ Campaign = apps.get_model('annoy', 'Campaign')
+ for b in Banner.objects.exclude(target=None):
+ if b.campaign is None:
+ b.campaign = Campaign.objects.create(name='auto')
+ b.save()
+ c = b.campaign
+ c.target = b.target
+ c.progress = b.progress
+ c.start = b.since
+ c.end = b.until
+ c.save()
+
+
+def data_campaign_rev(apps, schema_editor):
+ Banner = apps.get_model('annoy', 'Banner')
+ Campaign = apps.get_model('annoy', 'Campaign')
+ for c in Campaign.objects.exclude(target=None):
+ for b in c.banner_set.all():
+ b.target = c.target
+ b.progress = c.progress
+ b.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('annoy', '0020_campaign_end_campaign_landing_campaign_priority_and_more'),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ data_campaign,
+ data_campaign_rev
+ )
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2026-03-31 11:21
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('annoy', '0021_campaign_data'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='banner',
+ name='progress',
+ ),
+ migrations.RemoveField(
+ model_name='banner',
+ name='target',
+ ),
+ ]
class Campaign(models.Model):
- name = models.CharField(max_length=255, help_text='Dla zespołu')
+ name = models.CharField(max_length=255, help_text='Wewnętrzna nazwa kampanii')
image = models.FileField('obraz', upload_to='annoy/banners/', blank=True)
+ start = models.DateTimeField(null=True, blank=True)
+ end = models.DateTimeField(null=True, blank=True)
+ landing = models.CharField(blank=True, max_length=1024)
+ target = models.IntegerField('cel', null=True, blank=True)
+ progress = models.IntegerField('postęp', null=True, blank=True)
+ priority = models.PositiveSmallIntegerField(
+ 'priorytet', default=0,
+ help_text='Kampanie z wyższym priorytetem mają pierwszeństwo.')
def __str__(self):
return self.name
+ @classmethod
+ def get_active(cls):
+ n = now()
+ return cls.objects.exclude(start__gt=n).exclude(end__lt=n)
+
+ def has_progress(self):
+ return self.target is not None and self.start is not None and self.end is not None
+
+ @property
+ def progress_percent(self):
+ if not self.target:
+ return 0
+ return (self.progress or 0) / self.target * 100
+
+ @property
+ def progress_percent_pretty(self):
+ return int(self.progress_percent)
+
+ def update_progress(self):
+ if not self.start or not self.end or not self.target:
+ return
+ Schedule = apps.get_model('club', 'Schedule')
+ PayUOrder = apps.get_model('club', 'PayUOrder')
+ progress = PayUOrder.objects.filter(
+ completed_at__gte=self.start,
+ completed_at__lte=self.end,
+ ).aggregate(c=models.Sum('schedule__amount'))['c'] or 0
+
+ for schedule in Schedule.objects.filter(
+ method='paypal',
+ expires_at__gt=self.start
+ ):
+ progress += schedule.n_paypal_payments(self.start, self.end) * schedule.amount
+ self.progress = progress
+ self.save(update_fields=['progress'])
+
+ @classmethod
+ def update_all_progress(cls):
+ n = now()
+ for obj in cls.get_active().exclude(target=None):
+ obj.update_progress()
+
class Banner(models.Model):
place = models.SlugField('miejsce', choices=PLACE_CHOICES)
until = models.DateTimeField('do', null=True, blank=True)
books = models.ManyToManyField('catalogue.Book', blank=True)
- target = models.IntegerField('cel', null=True, blank=True)
- progress = models.IntegerField('postęp', null=True, blank=True)
show_members = models.BooleanField('widoczny dla członków klubu', default=False)
staff_preview = models.BooleanField('podgląd tylko dla zespołu', default=False)
only_authenticated = models.BooleanField('tylko dla zalogowanych', default=False)
else:
return self.image
+ def get_url(self):
+ if self.url:
+ return self.url
+ if self.campaign:
+ return self.campaign.landing
+
def is_external(self):
return (self.url and
not self.url.startswith('/') and
n = now()
banners = cls.objects.filter(
place=place
+ ).exclude(
+ campaign__start__gt=n
+ ).exclude(
+ campaign__end__lt=n
).exclude(
since__gt=n
).exclude(
until__lt=n
- ).order_by('-priority', '?')
+ ).order_by('-campaign__priority', '-priority', '?')
if book is None:
banners = banners.filter(books=None)
return banners
- @property
- def progress_percent(self):
- if not self.target:
- return 0
- return (self.progress or 0) / self.target * 100
-
- @property
- def progress_percent_pretty(self):
- return int(self.progress_percent)
-
- def update_progress(self):
- if not self.since or not self.until or not self.target:
- return
- Schedule = apps.get_model('club', 'Schedule')
- PayUOrder = apps.get_model('club', 'PayUOrder')
- progress = PayUOrder.objects.filter(
- completed_at__gte=self.since,
- completed_at__lte=self.until,
- ).aggregate(c=models.Sum('schedule__amount'))['c'] or 0
-
- for schedule in Schedule.objects.filter(
- method='paypal',
- expires_at__gt=self.since
- ):
- progress += schedule.n_paypal_payments(self.since, self.until) * schedule.amount
- self.progress = progress
- self.save(update_fields=['progress'])
-
- @classmethod
- def update_all_progress(cls):
- for obj in cls.objects.exclude(target=None).exclude(until__lt=now()):
- obj.update_progress()
class DynamicTextInsert(models.Model):
@receiver(post_save, sender=club.models.Schedule)
def update_progress(sender, instance, **kwargs):
try:
- models.Banner.update_all_progress()
+ models.Campaign.update_all_progress()
except:
pass
{% if banner %}
{% if closable %}
<a class='annoy-banner-on annoy-banner-on_{{ banner.place }}'
- href="{{ banner.url }}"
+ href="{{ banner.get_url }}"
data-target="#annoy-banner-{{ banner.id }}"
>
{{ banner.open_label }}
{% if not banner.action_label %}
<a
{% if banner.is_external %}target="_blank"{% endif %}
- href="{{ banner.url }}">
+ href="{{ banner.get_url }}">
{% endif %}
<div class="annoy-banner-inner">
{% if banner.action_label %}
<a class="action"
{% if banner.is_external %}target="_blank"{% endif %}
- href="{{ banner.url }}">
+ href="{{ banner.get_url }}">
{{ banner.action_label }}
</a>
{% endif %}
{% if banner %}
{% if closable %}
<a class='annoy-banner-on annoy-banner-on_{{ banner.place }}'
- href="{{ banner.url }}"
+ href="{{ banner.get_url }}"
data-target="#annoy-banner-{{ banner.id }}"
>
</a>
<div class="text">
{{ banner.get_text|safe|linebreaks }}
<div class="annoy-banner_actions">
- <a class="action l-button l-button--default l-button--default--dark" href="{{ banner.url }}">
+ <a class="action l-button l-button--default l-button--default--dark" href="{{ banner.get_url }}">
{{ banner.action_label }}
</a>
<a class='annoy-banner-off annoy-banner-off_{{ banner.place }} l-button l-button--default'>{{ banner.close_label|default:"x" }}</a>
{{ banner.get_text|safe|linebreaks }}
</div>
+ {% if banner.campaign.has_progress %}
<div class="state-box">
<div class="progress-box">
<div>
<div class="l-checkout__support__bar">
- <span data-label="{{ banner.progress_percent_pretty }}%" style="width: {{ banner.progress_percent|stringformat:".3f" }}%;"></span>
+ <span data-label="{{ banner.campaign.progress_percent_pretty }}%" style="width: {{ banner.campaign.progress_percent|stringformat:".3f" }}%;"></span>
</div>
</div>
</div>
<div class="time-box">
- <strong class="countdown inline" data-until='{{ banner.until|date_to_utc:True|utc_for_js }}'> </strong>
+ <strong class="countdown inline" data-until='{{ banner.campaign.end|date_to_utc:True|utc_for_js }}'> </strong>
</div>
<div class="action-box">
{% if banner.action_label %}
- <a class="action" href="{{ banner.url }}">
+ <a class="action" href="{{ banner.get_url }}">
{{ banner.action_label }}
</a>
{% endif %}
</div>
</div>
+ {% endif %}
</div>
{% if not banner.action_label %}
<a
{% if banner.is_external %}target="_blank"{% endif %}
- href="{{ banner.url }}">
+ href="{{ banner.get_url }}">
{% endif %}
<div class="annoy-banner-inner">
{% if banner.action_label %}
<a class="action"
{% if banner.is_external %}target="_blank"{% endif %}
- href="{{ banner.url }}">
+ href="{{ banner.get_url }}">
{{ banner.action_label }}
</a>
{% endif %}
</a>
{% endif %}
+ {% if banner.campaign.has_progress %}
<div class="state-box">
<div class="progress-box">
<div>
<div class="l-checkout__support__bar">
- <span class="{% if banner.progress_percent < 15 %}little-progress{% endif %}" data-label="{{ banner.progress_percent|floatformat:"2" }}% ({{ banner.progress|floatformat:"g" }} zł)" style="width: {{ banner.progress_percent|stringformat:".3f" }}%;"></span>
+ <span class="{% if banner.campaign.progress_percent < 15 %}little-progress{% endif %}" data-label="{{ banner.campaign.progress_percent|floatformat:"2" }}% ({{ banner.campaign.progress|floatformat:"g" }} zł)" style="width: {{ banner.campaign.progress_percent|stringformat:".3f" }}%;"></span>
</div>
</div>
</div>
<div class="bar-status-box">
<div class="time-box">
- <strong class="countdown inline" data-until='{{ banner.until|date_to_utc:True|utc_for_js }}'> </strong>
+ <strong class="countdown inline" data-until='{{ banner.campaign.end|date_to_utc:True|utc_for_js }}'> </strong>
</div>
- {% if banner.target %}
- <strong>{{ banner.target|floatformat:"g" }} zł</strong>
- {% endif %}
+ <strong>{{ banner.campaign.target|floatformat:"g" }} zł</strong>
</div>
</div>
+ {% endif %}
</div>
<div class="state-box">
<div class="action-box">
{% if banner.action_label %}
- <a class="action" href="{{ banner.url }}">
+ <a class="action" href="{{ banner.get_url }}">
{{ banner.action_label }}
</a>
{% endif %}
});
$(".annoy-banner-clickable").each(function() {
- $(this).click(() => {
- $("a.action", this).click();
+ let btn = $("a.action", this)[0];
+ $(this).click((e) => {
+ if (e.target !== btn) {
+ btn.click();
+ }
});
});