• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

iplweb / bpp / 18634744198

19 Oct 2025 07:00PM UTC coverage: 31.618% (-29.9%) from 61.514%
18634744198

push

github

mpasternak
Merge branch 'release/v202510.1270'

657 of 9430 branches covered (6.97%)

Branch coverage included in aggregate %.

229 of 523 new or added lines in 42 files covered. (43.79%)

11303 existing lines in 316 files now uncovered.

14765 of 39346 relevant lines covered (37.53%)

0.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.82
src/bpp/admin/core.py
1
from decimal import Decimal
1✔
2
from hashlib import md5
1✔
3

4
from dal import autocomplete
1✔
5
from dal_select2.fields import Select2ListChoiceField, Select2ListCreateChoiceField
1✔
6
from django import forms
1✔
7
from django.conf import settings
1✔
8
from django.contrib import admin
1✔
9
from django.core.cache import cache
1✔
10
from django.db.models.fields import BLANK_CHOICE_DASH
1✔
11
from django.forms import NullBooleanField
1✔
12
from django.forms.widgets import HiddenInput
1✔
13

14
from bpp.admin.crossref_api_helpers import KorzystaZCrossRefAPIAutorInlineMixin
1✔
15
from bpp.admin.zglos_publikacje_helpers import KorzystaZNumeruZgloszeniaInlineMixin
1✔
16
from bpp.jezyk_polski import warianty_zapisanego_nazwiska
1✔
17
from bpp.models import (
1✔
18
    Autor,
19
    Dyscyplina_Naukowa,
20
    Jednostka,
21
    Kierunek_Studiow,
22
    Typ_Odpowiedzialnosci,
23
    Uczelnia,
24
)
25

26
UPOWAZNIENIE_PBN = "upowaznienie_pbn"
1✔
27

28

29
# Proste tabele
30

31

32
class DynamicAdminFilterMixin:
1✔
33
    dynamic_filter_counts_enable = True
1✔
34

35
    def get_urls(self):
1✔
36
        """
37
        Dodaje custom URL pattern dla endpointu licznika filtrów.
38
        """
39
        from django.urls import re_path
1✔
40

41
        urls = super().get_urls()
1✔
42

43
        custom_urls = [
1✔
44
            re_path(
45
                r"^filter-count/$",
46
                self.admin_site.admin_view(self.filter_count_view),
47
                name=f"{self.model._meta.app_label}_{self.model._meta.model_name}_filter_count",
48
            ),
49
        ]
50

51
        return custom_urls + urls
1✔
52

53
    def filter_count_view(self, request):
1✔
54
        """
55
        Zwraca liczbę obiektów dla danego query stringa filtru jako plain text.
56

57
        Wynik jest cache'owany na 1 godzinę (3600s) z uwzględnieniem pełnego query stringa.
58
        Różne kombinacje filtrów mają różne cache keys.
59

60
        Używane przez HTMX do lazy loadingu liczników w filtrach admin changelist.
61
        """
62
        from django.http import HttpResponse
×
63

64
        try:
×
65
            # Wygeneruj unikalny cache key bazując na modelu i query stringu
NEW
66
            query_string = request.GET.urlencode()
×
NEW
67
            query_hash = md5(query_string.encode()).hexdigest()
×
NEW
68
            model_label = self.model._meta.label
×
NEW
69
            cache_key = f"filter_count_{model_label}_{query_hash}"
×
70

71
            # Sprawdź czy wynik jest już w cache
NEW
72
            count = cache.get(cache_key)
×
73

NEW
74
            if count is None:
×
75
                # Pobierz ChangeList instance z obecnym requestem (zawiera query string z filtrami)
NEW
76
                cl = self.get_changelist_instance(request)
×
77

78
                # Pobierz queryset z zastosowanymi filtrami z query stringa
79
                # ChangeList automatycznie parsuje query string i aplikuje filtry
NEW
80
                queryset = cl.get_queryset(request)
×
81

82
                # Policz obiekty
NEW
83
                count = queryset.count()
×
84

85
                # Zapisz w cache na 1 godzinę (3600 sekund)
NEW
86
                cache.set(cache_key, count, 3600)
×
87

88
            # Zwróć cyfrę jako HTML dla HTMX innerHTML
89
            return HttpResponse(f"{count}", content_type="text/html; charset=utf-8")
×
90
        except Exception:
×
91
            # W przypadku błędu zwróć myślnik
92
            return HttpResponse("-", content_type="text/html; charset=utf-8")
×
93

94

95
class BaseBppAdminMixin(DynamicAdminFilterMixin):
1✔
96
    """Ta klasa jest potrzebna, (XXXżeby działały sygnały post_commit.XXX)
97

98
    Ta klasa KIEDYŚ była potrzebna, obecnie niespecjalnie. Aczkolwiek,
99
    zostawiam ją z przyczyn historycznych, w ten sposób można łatwo
100
    wyłowić klasy edycyjne, które grzebią COKOLWIEK w cache.
101
    """
102

103
    # Mój dynks do grappelli
104
    auto_open_collapsibles = True
1✔
105

106
    # ograniczenie wielkosci listy
107
    list_per_page = 50
1✔
108

109

110
def get_first_typ_odpowiedzialnosci():
1✔
111
    return Typ_Odpowiedzialnosci.objects.filter(skrot="aut.").first()
1✔
112

113

114
def generuj_formularz_dla_autorow(  # noqa
1✔
115
    baseModel,
116
    include_rekord=False,
117
    include_dyscyplina=True,
118
):
119
    class baseModel_AutorForm(forms.ModelForm):
1✔
120
        if include_rekord:
1✔
121
            rekord = forms.ModelChoiceField(
1✔
122
                widget=HiddenInput, queryset=baseModel.rekord.get_queryset()
123
            )
124

125
        autor = forms.ModelChoiceField(
1✔
126
            queryset=Autor.objects.all(),
127
            widget=autocomplete.ModelSelect2(url="bpp:autor-autocomplete"),
128
        )
129

130
        jednostka = forms.ModelChoiceField(
1✔
131
            queryset=Jednostka.objects.all(),
132
            widget=autocomplete.ModelSelect2(url="bpp:jednostka-autocomplete"),
133
        )
134

135
        kierunek_studiow = forms.ModelChoiceField(
1✔
136
            label="Kierunek studiów",
137
            help_text="W przypadku autorów-studentów, możesz wpisać tu kierunek studiów, na jakim się znajdują. "
138
            "Pole opcjonalne. ",
139
            queryset=Kierunek_Studiow.objects.all(),
140
            required=False,
141
        )
142

143
        if include_dyscyplina:
1!
144
            dyscyplina_naukowa = forms.ModelChoiceField(
1✔
145
                queryset=Dyscyplina_Naukowa.objects.all(),
146
                widget=autocomplete.ModelSelect2(
147
                    forward=["autor", "rok"],
148
                    url="bpp:dyscyplina-naukowa-przypisanie-autocomplete",
149
                ),
150
                required=False,
151
            )
152

153
        zapisany_jako = Select2ListChoiceField(
1✔
154
            widget=autocomplete.Select2(
155
                url="bpp:zapisany-jako-autocomplete", forward=["autor"]
156
            ),
157
        )
158

159
        typ_odpowiedzialnosci = forms.ModelChoiceField(
1✔
160
            queryset=Typ_Odpowiedzialnosci.objects.all(),
161
            initial=get_first_typ_odpowiedzialnosci,
162
        )
163

164
        oswiadczenie_ken = forms.NullBooleanField(
1✔
165
            label="Oświadczenie KEN",
166
            help_text="Oświadczenie Komisji Ewaluacji Nauki (Uniwersytet Medyczny w Lublinie). "
167
            "Wyklucza Upoważnienie PBN. ",
168
        )
169

170
        def __init__(self, *args, **kwargs):  # noqa
1✔
171
            super().__init__(*args, **kwargs)
1✔
172

173
            # Ustaw inicjalną wartość dla pola 'afiliuje'
174
            domyslnie_afiliuje = True
1✔
175
            uczelnia = Uczelnia.objects.first()
1✔
176
            if uczelnia is not None:
1✔
177
                domyslnie_afiliuje = uczelnia.domyslnie_afiliuje
1✔
178
            self.fields["afiliuje"].initial = domyslnie_afiliuje
1✔
179

180
            # Nowy rekord
181
            instance = kwargs.get("instance")
1✔
182
            data = kwargs.get("data")
1✔
183
            if not data and not instance:
1✔
184
                if kwargs.get("initial"):
1✔
185
                    self.initial = kwargs.get("initial")
1✔
186
                    autor = self.initial.get("autor")
1✔
187
                else:
188
                    try:
1✔
189
                        autor = int(args[0]["autor"][0])
1✔
190
                    except (TypeError, ValueError, IndexError):
1✔
191
                        autor = None
1✔
192

193
                if autor is not None:
1✔
194
                    if isinstance(autor, int):
1!
195
                        try:
1✔
196
                            autor = Autor.objects.get(pk=int(autor))
1✔
197
                        except Autor.DoesNotExist:
1✔
198

199
                            class autor:
1✔
200
                                imiona = "TakiAutor"
1✔
201
                                nazwisko = "NieIstnieje"
1✔
202
                                poprzednie_nazwiska = ""
1✔
203

204
                    warianty = warianty_zapisanego_nazwiska(
1✔
205
                        autor.imiona, autor.nazwisko, autor.poprzednie_nazwiska
206
                    )
207
                    warianty = list(warianty)
1✔
208

209
                    if self.initial.get("zapisany_jako", "") not in warianty:
1!
210
                        warianty.append(self.initial.get("zapisany_jako"))
1✔
211

212
                    self.fields["zapisany_jako"] = Select2ListCreateChoiceField(
1✔
213
                        choice_list=list(warianty),
214
                        initial=self.initial.get("zapisany_jako"),
215
                        widget=autocomplete.Select2(
216
                            url="bpp:zapisany-jako-autocomplete", forward=["autor"]
217
                        ),
218
                    )
219

220
                return
1✔
221

222
            initial = None
1✔
223

224
            if instance:
1✔
225
                autor = instance.autor
1✔
226
                initial = instance.zapisany_jako
1✔
227

228
            if data:
1✔
229
                # "Nowe" dane z formularza przyszły
230
                zapisany_jako = data.get(kwargs["prefix"] + "-zapisany_jako")
1✔
231
                if not zapisany_jako:
1!
232
                    return
×
233

234
                try:
1✔
235
                    autor = Autor.objects.get(pk=int(data[kwargs["prefix"] + "-autor"]))
1✔
236
                except Autor.DoesNotExist:
×
237

238
                    class autor:
×
239
                        imiona = "TakiAutor"
×
240
                        nazwisko = "NieIstnieje"
×
241
                        poprzednie_nazwiska = ""
×
242

243
            warianty = warianty_zapisanego_nazwiska(
1✔
244
                autor.imiona, autor.nazwisko, autor.poprzednie_nazwiska
245
            )
246
            warianty = list(warianty)
1✔
247

248
            if initial not in warianty and instance is not None:
1✔
249
                warianty.append(instance.zapisany_jako)
1✔
250

251
            self.initial["zapisany_jako"] = initial
1✔
252

253
            self.fields["zapisany_jako"] = Select2ListCreateChoiceField(
1✔
254
                choice_list=list(warianty),
255
                initial=initial,
256
                widget=autocomplete.Select2(
257
                    url="bpp:zapisany-jako-autocomplete", forward=["autor"]
258
                ),
259
            )
260

261
            include_oswiadczenie_ken = getattr(
1✔
262
                settings, "BPP_POKAZUJ_OSWIADCZENIE_KEN", False
263
            )
264
            if not include_oswiadczenie_ken:
1✔
265
                self.fields["oswiadczenie_ken"] = NullBooleanField(widget=HiddenInput())
1✔
266

267
        class Media:
1✔
268
            js = ["/static/bpp/js/autorform_dependant.js"]
1✔
269

270
        class Meta:
1✔
271
            DATA_OSWIADCZENIA = "data_oswiadczenia"
1✔
272

273
            fields = [
1✔
274
                "autor",
275
                "jednostka",
276
                "kierunek_studiow",
277
                "typ_odpowiedzialnosci",
278
                "zapisany_jako",
279
                "afiliuje",
280
                "zatrudniony",
281
                UPOWAZNIENIE_PBN,
282
                "oswiadczenie_ken",
283
                "procent",
284
                "profil_orcid",
285
                DATA_OSWIADCZENIA,
286
                "kolejnosc",
287
            ]
288

289
            if include_dyscyplina:
1!
290
                data_oswiadczenia_idx = fields.index(DATA_OSWIADCZENIA)
1✔
291
                fields = (
1✔
292
                    fields[:data_oswiadczenia_idx]
293
                    + [
294
                        "dyscyplina_naukowa",
295
                        "przypieta",
296
                    ]
297
                    + fields[data_oswiadczenia_idx:]
298
                )
299

300
            if include_rekord:
1✔
301
                fields = [
1✔
302
                    "rekord",
303
                ] + fields
304

305
            model = baseModel
1✔
306
            widgets = {"kolejnosc": HiddenInput, "rekord": HiddenInput}
1✔
307

308
    return baseModel_AutorForm
1✔
309

310

311
def generuj_inline_dla_autorow(baseModel, include_dyscyplina=True):
1✔
312
    MAKSYMALNA_ILOSC_AUTOROW_W_FORMULARZU = 25
1✔
313

314
    class baseModel_AutorFormset(forms.BaseInlineFormSet):
1✔
315
        def get_queryset(self):
1✔
316
            qs = super().get_queryset()
1✔
317
            return qs[:MAKSYMALNA_ILOSC_AUTOROW_W_FORMULARZU]
1✔
318

319
        def clean(self):
1✔
320
            # get forms that actually have valid data
321
            percent = Decimal("0.00")
1✔
322
            for form in self.forms:
1✔
323
                try:
1✔
324
                    if form.cleaned_data:
1!
325
                        percent += form.cleaned_data.get(
1✔
326
                            "procent", Decimal("0.00")
327
                        ) or Decimal("0.00")
328
                except AttributeError:
×
329
                    # annoyingly, if a subform is invalid Django explicity raises
330
                    # an AttributeError for cleaned_data
331
                    pass
×
332
            if percent > Decimal("100.00"):
1!
333
                raise forms.ValidationError(
×
334
                    "Liczba podanych procent odpowiedzialności przekracza 100.0"
335
                )
336

337
    baseClass = admin.StackedInline
1✔
338
    extraRows = 0
1✔
339

340
    from django.conf import settings
1✔
341

342
    if getattr(settings, "INLINE_DLA_AUTOROW", "stacked") == "tabular":
1!
343
        baseClass = admin.TabularInline
×
344
        extraRows = 1
×
345

346
    class baseModel_AutorInline(
1✔
347
        KorzystaZNumeruZgloszeniaInlineMixin,
348
        KorzystaZCrossRefAPIAutorInlineMixin,
349
        baseClass,
350
    ):
351
        model = baseModel
1✔
352
        extra = extraRows
1✔
353
        form = generuj_formularz_dla_autorow(
1✔
354
            baseModel,
355
            include_rekord=False,
356
            include_dyscyplina=include_dyscyplina,
357
        )
358
        formset = baseModel_AutorFormset
1✔
359
        sortable_field_name = "kolejnosc"
1✔
360
        sortable_excludes = [
1✔
361
            "typ_odpowiedzialnosci",
362
            "zapisany_jako",
363
            "afiliuje",
364
        ]
365

366
        # Maksymalna ilosć autorów edytowanych w ramach formularza. Pozostałych
367
        # autorów nalezy edytować przez opcję "Edycja autorów"
368
        max_num = MAKSYMALNA_ILOSC_AUTOROW_W_FORMULARZU
1✔
369
        verbose_name_plural = (
1✔
370
            f"Powiązania autorów z rekordem - jeżeli potrzebujesz więcej, jak {max_num}, "
371
            f"edytuj je przy pomocy przycisku 'Autorzy' na górze formularza"
372
        )
373

374
    return baseModel_AutorInline
1✔
375

376

377
#
378
# Kolumny ze skrótami
379
#
380

381

382
class KolumnyZeSkrotamiMixin:
1✔
383
    def charakter_formalny__skrot(self, obj):
1✔
UNCOV
384
        return obj.charakter_formalny.skrot
×
385

386
    charakter_formalny__skrot.short_description = "Char. form."
1✔
387
    charakter_formalny__skrot.admin_order_field = "charakter_formalny__skrot"
1✔
388

389
    def typ_kbn__skrot(self, obj):
1✔
UNCOV
390
        return obj.typ_kbn.skrot
×
391

392
    typ_kbn__skrot.short_description = "typ MNiSW/MEiN"
1✔
393
    typ_kbn__skrot.admin_order_field = "typ_kbn__skrot"
1✔
394

395

396
class RestrictDeletionToAdministracjaGroupMixin:
1✔
397
    def get_action_choices(self, request, default_choices=BLANK_CHOICE_DASH):
1✔
398
        if "administracja" in [x.name for x in request.user.cached_groups]:
×
399
            return admin.ModelAdmin.get_action_choices(self, request, default_choices)
×
400
        return []
×
401

402
    def has_delete_permission(self, request, obj=None):
1✔
403
        if "administracja" in [x.name for x in request.user.cached_groups]:
1!
UNCOV
404
            return admin.ModelAdmin.has_delete_permission(self, request, obj=obj)
×
405
        return False
1✔
406

407

408
class RestrictDeletionToAdministracjaGroupAdmin(
1✔
409
    RestrictDeletionToAdministracjaGroupMixin, admin.ModelAdmin
410
):
411
    pass
1✔
412

413

414
class PreventDeletionMixin:
1✔
415
    def has_delete_permission(self, request, obj=None):
1✔
416
        return False
1✔
417

418
    def get_action_choices(self, request, default_choices=BLANK_CHOICE_DASH):
1✔
419
        return []
×
420

421

422
class PreventDeletionAdmin(PreventDeletionMixin, admin.ModelAdmin):
1✔
423
    pass
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc