Simpler payments and introduce seasonal banner.
authorRadek Czajka <rczajka@rczajka.pl>
Mon, 23 Mar 2026 11:32:32 +0000 (12:32 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Mon, 23 Mar 2026 11:32:32 +0000 (12:32 +0100)
14 files changed:
src/annoy/models.py
src/annoy/places.py
src/annoy/templates/annoy/banner_seasonal.html [new file with mode: 0644]
src/annoy/templatetags/annoy.py
src/club/forms.py
src/club/models.py
src/club/templates/club/donation_step1.html
src/club/templates/club/donation_step2.html
src/club/templates/club/donation_step_base.html
src/wolnelektury/static/2022/styles/layout/_annoy.scss
src/wolnelektury/static/2022/styles/layout/_checkout.scss
src/wolnelektury/static/js/book_text/references.js
src/wolnelektury/static/js/main.js
src/wolnelektury/templates/base.html

index 6af1cc0..638da5e 100644 (file)
@@ -120,20 +120,26 @@ class Banner(models.Model):
         return int(self.progress_percent)
 
     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']
+        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']
+
+        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):
+        for obj in cls.objects.exclude(target=None).exclude(until__lt=now()):
             obj.update_progress()
 
 
index 0de0483..2fbd34c 100644 (file)
@@ -13,6 +13,7 @@ PLACE_DEFINITIONS = [
         ('quiet', 'Spokojny'),
         ('loud', 'Ostry'),
     )),
+    ('seasonal', 'Sezonowa', False),
 ]
 
 PLACE_CHOICES = [p[:2] for p in PLACE_DEFINITIONS]
diff --git a/src/annoy/templates/annoy/banner_seasonal.html b/src/annoy/templates/annoy/banner_seasonal.html
new file mode 100644 (file)
index 0000000..a3a8a1a
--- /dev/null
@@ -0,0 +1,71 @@
+{% load l10n %}
+{% load time_tags %}
+
+{% if banner %}
+<div class="l-container">
+  <div class="
+              annoy-banner
+              annoy-banner_{{ banner.place }}
+              annoy-banner-style_{{ banner.style }}
+              {% if banner.get_image %}with-image{% endif %}
+              {% if banner.smallfont %}banner-smallfont{% endif %}
+              annoy-banner-clickable
+              "
+        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 %}
+        ">
+    {% if not banner.action_label %}
+    <a
+      {% if banner.is_external %}target="_blank"{% endif %}
+      href="{{ banner.url }}">
+    {% endif %}
+    <div class="annoy-banner-inner">
+
+      {% if banner.get_image %}
+      <div>
+        <img src="{{ banner.get_image.url }}">
+       </div>
+      {% endif %}
+      <div class="text">
+        {{ banner.get_text|safe|linebreaks }}
+      </div>
+
+
+
+
+      {% if banner.action_label %}
+      <a class="action"
+        {% if banner.is_external %}target="_blank"{% endif %}
+        href="{{ banner.url }}">
+          {{ banner.action_label }}
+        </a>
+      {% endif %}
+    </div>
+    {% if not banner.action_label %}
+      </a>
+    {% endif %}
+
+       <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_pretty }}%" style="width: {{ banner.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 }}'>&nbsp;</strong>
+         </div>
+          {% if banner.target %}
+          <strong>{{ banner.target }} zł</strong>
+          {% endif %}
+          </div>
+       </div>
+
+
+  </div>
+</div>
+{% endif %}
index cdf4dbc..602fb3c 100644 (file)
@@ -45,3 +45,11 @@ def annoy_banner_crisis(context):
         'banner': banners.first(),
         'closable': True,
     }
+
+@register.inclusion_tag('annoy/banner_seasonal.html', takes_context=True)
+def annoy_banner_seasonal(context):
+    banners = Banner.choice('seasonal', request=context['request'], exemptions=False)
+    return {
+        'banner': banners.first(),
+        'closable': False,
+    }
index e096303..ceda8d5 100644 (file)
@@ -62,9 +62,7 @@ class DonationStep2Form(forms.ModelForm, NewsletterForm):
         model = models.Schedule
         fields = [
             'first_name', 'last_name',
-            'email', 'phone',
-            'postal',
-            'postal_code', 'postal_town', 'postal_country',
+            'email',
             ]
         widgets = {
             'amount': forms.HiddenInput,
@@ -74,16 +72,14 @@ class DonationStep2Form(forms.ModelForm, NewsletterForm):
     def __init__(self, **kwargs):
         super().__init__(**kwargs)
 
-        self.fields['first_name'].required = True
-        self.fields['last_name'].required = True
-        
         self.consent = []
         for c in models.Consent.objects.filter(active=True).order_by('order'):
             key = f'consent{c.id}'
-            self.fields[key] = forms.BooleanField(
-                label=c.text,
-                required=c.required
-            )
+            if not c.required:
+                self.fields[key] = forms.BooleanField(
+                    label=c.text,
+                    required=c.required
+                )
             self.consent.append((
                 c, key, (lambda k: lambda: self[k])(key)
             ))
index de2d3c6..eb14390 100644 (file)
@@ -201,6 +201,20 @@ class Schedule(models.Model):
     def is_recurring(self):
         return self.monthly or self.yearly
 
+    def n_paypal_payments(self, since, until):
+        # TODO: pull BA payments.
+        t = self.payed_at
+        if t is None: return 0
+        c = 0
+        until = min(until, now())
+        t += timedelta(days=1)
+        while t < until:
+            if t >= since:
+                c += 1
+            m = datetime(t.year, t.month, 1)
+            t += ((m + timedelta(days=31)).replace(day=1)) - m
+        return c
+
     def set_payed(self):
         since = self.expires_at
         n = now()
index c9f5ed2..d01e693 100644 (file)
@@ -1,5 +1,13 @@
 {% extends 'club/donation_step_base.html' %}
+{% load chunks %}
 
 {% block donation-step-content %}
   {% include "club/donation_step1_form.html" %}
 {% endblock %}
+
+{% block donate-bottom %}
+<a name="wiecej"></a>
+<div class="donation-more">
+  {% chunk "donate-bottom" %}
+</div>
+{% endblock %}
index 5d60213..0a4cba5 100644 (file)
         {% csrf_token %}
         {{ form.errors }}
         <div class="l-checkout__form">
-          <div class="l-checkout__form__row">
-            <div class="l-checkout__input">
-              <label for="id_first_name"><span>*</span> {% trans "Imię" %}</label>
-              {{ form.first_name }}
-              {{ form.first_name.errors }}
-            </div>
-            <div class="l-checkout__input">
-              <label for="id_last_name"><span>*</span> {% trans "Nazwisko" %}</label>
-              {{ form.last_name }}
-              {{ form.last_name.errors }}
-            </div>
-          </div>
-          <div class="l-checkout__form__row">
-            <div class="l-checkout__input">
+          <div class="l-checkout__form__row full">
+            <div class="l-checkout__input" lang="pl">
               <label for="id_email"><span>*</span> {% trans "E-mail" %}</label>
               {{ form.email }}
               {{ form.email.errors }}
             </div>
-            <div class="l-checkout__input">
-              <label for="id_phone">{% trans "Telefon" %}</label>
-              {{ form.phone }}
-              {{ form.phone.errors }}
-            </div>
-          </div>
-          <div class="l-checkout__form__row full">
-            <div class="l-checkout__input">
-              <label for="id_postal">{% trans "Adres pocztowy" %}</label>
-              {{ form.postal }}
-              {{ form.postal.errors }}
-            </div>
           </div>
           <div class="l-checkout__form__row">
             <div class="l-checkout__input">
-              <label for="id_postal_code">{% trans "Kod pocztowy" %}</label>
-              {{ form.postal_code }}
-              {{ form.postal_code.errors }}
-            </div>
-            <div class="l-checkout__input">
-              <label for="id_postal_town">{% trans "Miejscowość" %}</label>
-              {{ form.postal_town }}
-              {{ form.postal_town.errors }}
+              <label for="id_first_name">{% trans "Imię / pseudonim (opcjonalnie)" %}</label>
+              {{ form.first_name }}
+              {{ form.first_name.errors }}
             </div>
-          </div>
-          <div class="l-checkout__form__row full">
             <div class="l-checkout__input">
-              <label for="id_postal_country">{% trans "Kraj" %}</label>
-              {{ form.postal_country }}
-              {{ form.postal_country.errors }}
+              <label for="id_last_name">{% trans "Nazwisko (opcjonalnie)" %}</label>
+              {{ form.last_name }}
+              {{ form.last_name.errors }}
             </div>
           </div>
+          
           <div class="l-checkout__form__row full">
             {% for consent, key, field in form.consent %}
-              {{ field.errors }}
+            {% if not consent.required %}
+            {{ field.errors }}
+            {% endif %}
               <div class="c-checkbox">
+                {% if consent.required %}
+                <label><p class="scroll">{{ consent.text|safe }}</p></label>
+                {% else %}
                 {{ field }}
                 <label for="id_{{ key }}">
-                  <p>{% if field.field.required %}<span>*</span> {% endif %}{{ field.label }}</p>
+                  <p>{% if field.field.required %}<span>*</span> {% endif %}{{ field.label|safe }}</p>
                 </label>
+                {% endif %}
               </div>
             {% endfor %}
             <div class="c-checkbox">
               {{ form.agree_newsletter }}
               <label for="id_agree_newsletter">
-                <p>{% trans "Zapisuję się na newsletter." %}</p>
+                <p>{% trans "Chcę dostawać newsletter o działalności Wolnych Lektur" %}</p>
               </label>
             </div>
           </div>
index 3889324..0c99d3c 100644 (file)
@@ -84,8 +84,9 @@
 
     <div class="l-checkout__footer">
       <div class="l-checkout__footer__content">
-        {% chunk 'donate-bottom' %}
+        {% block donate-bottom %}{% endblock %}
 
+        <a name="informacje"></a>
         <div class="l-checkout__footer__content__item">
           <h3>{% trans "Transparentność jest dla nas bardzo ważna." %}</h3>
           <div>
index 5c76498..f6b619a 100644 (file)
     }
 }
 
+
 .annoy-banner_book-page-center {
     background: white;
     margin-top: 50px;
        }
     }
 }
+
+
+
+
+
+.annoy-banner_seasonal {
+    background-color: #ffd430;
+    color: #083F4D;
+    border-radius: 10px;
+    padding: 15px 20px;
+    margin-top: 20px;
+    cursor: pointer;
+
+    .annoy-banner-inner {
+       display: flex;
+       flex-direction: row;
+       gap: 20px;
+       align-items: flex-start;
+       justify-content: space-between;
+
+       p {
+           margin: 0;
+       }
+       a {
+           line-height: 1.35;
+           color: #c32721;
+           white-space: nowrap;
+           border: solid #c32721;
+           border-width: 0 0 1px;
+
+           &:hover {
+               text-decoration: none;
+               border-bottom-width: 2px;
+           }
+       }
+        a.action { 
+            color: #fff;
+            background: #c92834;
+            padding: 9px 20px;
+            font-weight: 600;
+            border-radius: 15px;
+           border-width: 0;
+           &:hover {
+               border-bottom-width: 0;
+            }
+        }
+
+    }
+        .state-box {
+            margin-top: 15px;
+        }
+        .bar-status-box {
+            display: flex;
+            justify-content: space-between;
+        }
+}
index 888df46..71d6e82 100644 (file)
       flex-direction: row;
   }
   
-  &:nth-child(4) {
-    .l-checkout__input {
-      &:nth-child(1) { width: 172px; }
-      &:nth-child(2) { width: calc(100% - 172px); }
-    }
-  }
-
   &.full {
     flex-direction: column;
     @include rwd($break-flow) {
     border: 1px solid #edc016;
     background: #edc016;
 }
+
+
+.donation-more {
+    margin-bottom:40px;
+}
index 3f5b7e3..ee7e2bd 100644 (file)
                         history.pushState({}, '', anchor);
                     },
                 });
+                return true;
             }
         }
     }
     scrollToAnchor(window.location.hash)
-    $('#toc, #themes, #book-text, #annotation').on('click', 'a', function(event) {
-        event.preventDefault();
-        scrollToAnchor($(this).attr('href'));
+    $('#toc, #themes, #book-text, #annotation, .scroll').on('click', 'a', function(event) {
+        if (scrollToAnchor($(this).attr('href'))) {
+            event.preventDefault();
+        }
     });
-
     
 })})(jQuery);
index 666d7a0..b224511 100644 (file)
         $(".c-media__settings").toggleClass('active');
     });
 
-    const crisis = document.querySelector(".annoy-banner_crisis-container");
-    const crisisLink = document.querySelector('.annoy-banner_crisis-container a.action');
-    if (crisis) {
-       crisis.addEventListener("click", function() {
-           crisisLink.click();
-       });
-    }
+    $(".annoy-banner-clickable").each(function() {
+        $(this).click(() => {
+            $("a.action", this).click();
+        });
+    });
 
 })();
index f1a7bf6..5b9b8ea 100644 (file)
@@ -6,11 +6,12 @@
 {% load funding_tags %}
 {% load piwik_tags %}
 {% load title %}
+{% load annoy %}
 
 {% block settings %}
 {% endblock %}
 
-<html class="no-js">
+<html class="no-js" lang="{{ LANGUAGE_CODE }}">
   <head>
     <meta charset="utf-8">
     <meta name="description" content="">
@@ -38,6 +39,8 @@
       </div>
     {% endif %}
 
+    {% annoy_banner_seasonal %}
+      
     {% block global-content %}
       <div class="l-container l-breadcrumb-container">
         <div class="l-breadcrumb">