class AnnoyConfig(AppConfig):
name = 'annoy'
+
+ def ready(self):
+ from . import signals
--- /dev/null
+# Generated by Django 4.0.8 on 2024-12-04 11:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('annoy', '0016_alter_mediainsertset_file_format'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='banner',
+ name='progress',
+ field=models.IntegerField(blank=True, null=True, verbose_name='postęp'),
+ ),
+ migrations.AddField(
+ model_name='banner',
+ 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-text-intermission', 'Przerwa w treści książki'), ('book-fragment-list', 'Obok listy fragmentów książki'), ('blackout', 'Blackout'), ('crisis', 'Kryzysowa')], verbose_name='miejsce'),
+ ),
+ ]
help_text='Bannery z wyższym priorytetem mają pierwszeństwo.')
since = models.DateTimeField('od', null=True, blank=True)
until = models.DateTimeField('do', null=True, 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)
return Template(self.text).render(Context())
@classmethod
- def choice(cls, place, request):
+ def choice(cls, place, request, exemptions=True):
Membership = apps.get_model('club', 'Membership')
- if hasattr(request, 'annoy_banner_exempt'):
+ if exemptions and hasattr(request, 'annoy_banner_exempt'):
return cls.objects.none()
if settings.DEBUG:
return banners
+ @property
+ def progress_percent(self):
+ if not self.target:
+ return 0
+ return (self.progress or 0) / self.target * 100
+
+ def update_progress(self):
+ # Total of new payments during the action.
+ # This definition will need to change for longer timespans.
+ if not self.since or not self.until or not self.target:
+ return
+ Schedule = apps.get_model('club', 'Schedule')
+ self.progress = Schedule.objects.filter(
+ payed_at__gte=self.since,
+ payed_at__lte=self.until,
+ ).aggregate(c=models.Sum('amount'))['c']
+ self.save(update_fields=['progress'])
+
+ @classmethod
+ def update_all_progress(cls):
+ for obj in cls.objects.exclude(target=None):
+ obj.update_progress()
+
class DynamicTextInsert(models.Model):
paragraphs = models.IntegerField('akapity')
class Meta:
ordering = ('ordering',)
-
-
-from django.db.models.signals import post_save, post_delete
-from django.dispatch import receiver
-
-@receiver(post_delete, sender=MediaInsertText)
-@receiver(post_save, sender=MediaInsertText)
-def update_etag(sender, instance, **kwargs):
- instance.media_insert_set.update_etag()
# ('centre', 'Środek ekranu'),
('upper', 'Górna połowa ekranu'),
)),
+ ('crisis', 'Kryzysowa', False),
]
PLACE_CHOICES = [p[:2] for p in PLACE_DEFINITIONS]
--- /dev/null
+from django.db.models.signals import post_save, post_delete
+from django.dispatch import receiver
+import club.models
+from . import models
+
+
+@receiver(post_delete, sender=models.MediaInsertText)
+@receiver(post_save, sender=models.MediaInsertText)
+def update_etag(sender, instance, **kwargs):
+ instance.media_insert_set.update_etag()
+
+
+@receiver(post_save, sender=club.models.Schedule)
+def update_progress(sender, instance, **kwargs):
+ try:
+ models.Banner.update_all_progress()
+ except:
+ pass
--- /dev/null
+{% load l10n %}
+{% load time_tags %}
+
+{% if banner %}
+<div class="annoy-banner_crisis-container">
+ <div class="
+ annoy-banner
+ annoy-banner_{{ banner.place }}
+ annoy-banner-style_{{ banner.style }}
+ {% if banner.image %}with-image{% endif %}
+ {% if banner.smallfont %}banner-smallfont{% endif %}
+ "
+ id="annoy-banner-{{ banner.id }}"
+ style="
+ {% if banner.text_color %}color: {{ banner.text_color }};{% endif %}
+ {% if banner.background_color %}background-color: {{ banner.background_color }};{% endif %}
+
+ ">
+ <div class="annoy-banner-inner">
+
+ <div class="image-box">
+ {% if banner.image %}
+ <img src="{{ banner.image.url }}">
+ {% endif %}
+ </div>
+
+ <div class="text-box">
+ <div class="text">
+ {{ banner.get_text|safe|linebreaks }}
+ </div>
+
+ <div class="state-box">
+ <div class="progress-box">
+ <div>
+ <div class="l-checkout__support__bar">
+ <span data-label="{{ banner.progress_percent|floatformat:'0' }}%" style="width: {% localize off %}{{ banner.progress_percent }}%{% endlocalize %};"></span>
+ </div>
+ </div>
+ </div>
+ <div class="time-box">
+ <strong class="countdown inline" data-until='{{ banner.until|date_to_utc:True|utc_for_js }}'></strong>
+ </div>
+ <div class="action-box">
+ {% if banner.action_label %}
+ <a class="action" href="{{ banner.url }}">
+ {{ banner.action_label }}
+ </a>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+
+
+
+ </div>
+ </div>
+ </div>
+
+{% endif %}
+
'banners': Banner.choice(place, request=context['request']),
'closable': PLACES.get(place, False),
}
+
+
+@register.inclusion_tag('annoy/banner_crisis.html', takes_context=True)
+def annoy_banner_crisis(context):
+ banners = Banner.choice('crisis', request=context['request'], exemptions=False)
+ return {
+ 'banner': banners.first(),
+ 'closable': True,
+ }
<main class="l-main">
+ {% comment %}
+ <!-- TODO: hide when crisis banner on -->
<div class="l-checkout__support">
<div class="l-checkout__support__bar">
<span data-label="{% club_count_recurring as c %}{% blocktrans %}Jest nas {{ c }}{% endblocktrans %}" style="width: calc({{ c }}% / 5);"></span>
<p>{% blocktrans with c=500 %}Potrzebujemy <strong>{{ c }}</strong> regularnych darczyńców, by Wolne Lektury mogły działać!{% endblocktrans %}</p>
</div>
</div>
+ {% endcomment %}
<div class="l-checkout__box">
<div class="l-checkout__box__header">
}
}
}
+
+
+.annoy-banner_crisis-container {
+ position: sticky;
+ top: 0;
+ height: 160px;
+ z-index: 10;
+ box-shadow: 0 0 10px black;
+ display: flex;
+ background: #c32721;
+ color: black;
+ align-items:center;
+ cursor: pointer;
+
+ @media screen and (min-height: 480px) {
+ height: 33vh;
+ top: calc(-33vh + 160px);
+ }
+
+ @media screen and (max-width: 940px) {
+ padding: 10px 0;
+ height: auto;
+ top: 0;
+ }
+
+ .annoy-banner_crisis {
+ position: sticky;
+ top: 0;
+ width: 100%;
+
+ .annoy-banner-inner {
+ max-width: 1172px;
+ margin: auto;
+ padding-right: 16px;
+ padding-left: 16px;
+
+ display: flex;
+ gap: 20px;
+ align-items: flex-start;
+
+ .image-box {
+ position: relative;
+ img {
+ height: 160px;
+ display: block;
+
+ @media screen and (max-width: 700px) {
+ height: 120px;
+ }
+ }
+ }
+
+ .text-box {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ @media screen and (max-width: 700px) {
+ p {
+ font-size: .9em;
+ }
+ }
+
+ .text {
+ background: #edc016;
+ padding: 1em;
+ border: 3px solid black;
+ }
+ a {
+ color: #c32721;
+ }
+ .state-box {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ @media screen and (max-width: 700px) {
+ flex-direction: column;
+ align-items: stretch;
+ text-align: center;
+ }
+ .progress-box {
+ flex-grow: 1;
+
+ .l-checkout__support__bar span::after {
+ right: auto;
+ left: 5px;
+ color: black;
+ }
+ }
+ }
+ }
+
+ p {
+ margin: 0;
+ }
+
+ a.action {
+ background: #edc016;
+ color: black;
+ padding: .8em 1em;
+ border: 3px solid black;
+ border-radius: 10px;
+ display: block;
+ transition: background-color .2s;
+
+ &:hover {
+ background: #ffd430;
+ text-decoration: none;
+ }
+ }
+ }
+ }
+}
$(".c-media__settings").toggleClass('active');
});
+ const crisis = document.querySelector(".annoy-banner_crisis-container");
+ const crisisLink = document.querySelector('.annoy-banner_crisis-container a.action');
+ crisis.addEventListener("click", function() {
+ crisisLink.click();
+ });
+
})();
{% load latest_blog_posts from blog %}
{% load preview_ad from catalogue_tags %}
+{% annoy_banner_crisis %}
+
{% annoy_banner_blackout %}
<nav class="l-navigation">