• 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

18.41
src/pbn_api/client.py
1
import json
1✔
2
import random
1✔
3
import sys
1✔
4
import time
1✔
5
import warnings
1✔
6
from builtins import NotImplementedError
1✔
7
from pprint import pprint
1✔
8
from urllib.parse import parse_qs, quote, urlparse
1✔
9

10
import requests
1✔
11
import rollbar
1✔
12
from django.contrib.contenttypes.models import ContentType
1✔
13
from django.core.mail import mail_admins
1✔
14
from django.db import transaction
1✔
15
from django.db.models import Model
1✔
16
from django.utils.itercompat import is_iterable
1✔
17
from requests import ConnectionError
1✔
18
from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError
1✔
19
from requests.exceptions import SSLError
1✔
20
from simplejson.errors import JSONDecodeError
1✔
21

22
from import_common.core import (
1✔
23
    matchuj_aktualna_dyscypline_pbn,
24
    matchuj_nieaktualna_dyscypline_pbn,
25
)
26
from import_common.normalization import normalize_kod_dyscypliny
1✔
27
from pbn_api.adapters.wydawnictwo import (
1✔
28
    OplataZaWydawnictwoPBNAdapter,
29
    WydawnictwoPBNAdapter,
30
)
31
from pbn_api.const import (
1✔
32
    DEFAULT_BASE_URL,
33
    NEEDS_PBN_AUTH_MSG,
34
    PBN_DELETE_PUBLICATION_STATEMENT,
35
    PBN_GET_DISCIPLINES_URL,
36
    PBN_GET_INSTITUTION_PUBLICATIONS_V2,
37
    PBN_GET_INSTITUTION_STATEMENTS,
38
    PBN_GET_JOURNAL_BY_ID,
39
    PBN_GET_LANGUAGES_URL,
40
    PBN_GET_PUBLICATION_BY_ID_URL,
41
    PBN_POST_INSTITUTION_STATEMENTS_URL,
42
    PBN_POST_PUBLICATION_FEE_URL,
43
    PBN_POST_PUBLICATION_NO_STATEMENTS_URL,
44
    PBN_POST_PUBLICATIONS_URL,
45
    PBN_SEARCH_PUBLICATIONS_URL,
46
)
47
from pbn_api.exceptions import (
1✔
48
    AccessDeniedException,
49
    AuthenticationConfigurationError,
50
    AuthenticationResponseError,
51
    CannotDeleteStatementsException,
52
    HttpException,
53
    NeedsPBNAuthorisationException,
54
    NoFeeDataException,
55
    NoPBNUIDException,
56
    PBNUIDChangedException,
57
    PBNUIDSetToExistentException,
58
    PraceSerwisoweException,
59
    PublikacjaInstytucjiV2NieZnalezionaException,
60
    ResourceLockedException,
61
    SameDataUploadedRecently,
62
    ZnalezionoWielePublikacjiInstytucjiV2Exception,
63
)
64
from pbn_api.models import TlumaczDyscyplin
1✔
65
from pbn_api.models.discipline import Discipline, DisciplineGroup
1✔
66
from pbn_api.models.pbn_odpowiedzi_niepozadane import PBNOdpowiedziNiepozadane
1✔
67
from pbn_api.models.sentdata import SentData
1✔
68
from pbn_api.utils import rename_dict_key
1✔
69

70

71
def smart_content(content):
1✔
UNCOV
72
    try:
×
UNCOV
73
        return content.decode("utf-8")
×
UNCOV
74
    except UnicodeDecodeError:
×
UNCOV
75
        return content
×
76

77

78
class PBNClientTransport:
1✔
79
    def __init__(self, app_id, app_token, base_url, user_token=None):
1✔
UNCOV
80
        self.app_id = app_id
×
UNCOV
81
        self.app_token = app_token
×
82

UNCOV
83
        self.base_url = base_url
×
UNCOV
84
        if self.base_url is None:
×
UNCOV
85
            self.base_url = DEFAULT_BASE_URL
×
86

UNCOV
87
        self.access_token = user_token
×
88

89

90
class PageableResource:
1✔
91
    def __init__(self, transport, res, url, headers, body=None, method="get"):
1✔
UNCOV
92
        self.url = url
×
UNCOV
93
        self.headers = headers
×
UNCOV
94
        self.transport = transport
×
UNCOV
95
        self.body = body
×
UNCOV
96
        self.method = getattr(transport, method)
×
97

UNCOV
98
        try:
×
UNCOV
99
            self.page_0 = res["content"]
×
UNCOV
100
        except KeyError:
×
UNCOV
101
            self.page_0 = []
×
102

UNCOV
103
        self.current_page = res["number"]
×
UNCOV
104
        self.total_elements = res["totalElements"]
×
UNCOV
105
        self.total_pages = res["totalPages"]
×
UNCOV
106
        self.done = False
×
107

108
    def count(self):
1✔
UNCOV
109
        return self.total_elements
×
110

111
    def fetch_page(self, current_page):
1✔
UNCOV
112
        if current_page == 0:
×
UNCOV
113
            return self.page_0
×
114
        # print(f"FETCH {current_page}")
115

UNCOV
116
        kw = {"headers": self.headers}
×
UNCOV
117
        if self.body:
×
118
            kw["body"] = self.body
×
119

UNCOV
120
        ret = self.method(self.url + f"&page={current_page}", **kw)
×
121
        # print(f"FETCH DONE {current_page}")
122

UNCOV
123
        try:
×
UNCOV
124
            return ret["content"]
×
125
        except KeyError:
×
126
            return
×
127

128
    def __iter__(self):
1✔
UNCOV
129
        for n in range(0, self.total_pages):
×
UNCOV
130
            yield from self.fetch_page(n)
×
131

132

133
class OAuthMixin:
1✔
134
    @classmethod
1✔
135
    def get_auth_url(klass, base_url, app_id, state=None):
1✔
UNCOV
136
        url = f"{base_url}/auth/pbn/api/registration/user/token/{app_id}"
×
UNCOV
137
        if state:
×
UNCOV
138
            from urllib.parse import quote
×
139

UNCOV
140
            url += f"?state={quote(state)}"
×
UNCOV
141
        return url
×
142

143
    @classmethod
1✔
144
    def get_user_token(klass, base_url, app_id, app_token, one_time_token):
1✔
UNCOV
145
        headers = {
×
146
            "X-App-Id": app_id,
147
            "X-App-Token": app_token,
148
        }
UNCOV
149
        body = {"oneTimeToken": one_time_token}
×
UNCOV
150
        url = f"{base_url}/auth/pbn/api/user/token"
×
UNCOV
151
        response = requests.post(url=url, json=body, headers=headers)
×
UNCOV
152
        try:
×
UNCOV
153
            response.json()
×
NEW
154
        except ValueError as e:
×
UNCOV
155
            if response.content.startswith(b"Mismatched X-APP-TOKEN: "):
×
UNCOV
156
                raise AuthenticationConfigurationError(
×
157
                    "Token aplikacji PBN nieprawidłowy. Poproś administratora "
158
                    "o skonfigurowanie prawidłowego tokena aplikacji PBN w "
159
                    "ustawieniach obiektu Uczelnia. "
160
                ) from e
161

NEW
162
            raise AuthenticationResponseError(response.content) from e
×
163

UNCOV
164
        return response.json().get("X-User-Token")
×
165

166
    def authorize(self, base_url, app_id, app_token):
1✔
167
        from pbn_api.conf import settings
×
168

169
        if self.access_token:
×
170
            return True
×
171

172
        self.access_token = getattr(settings, "PBN_CLIENT_USER_TOKEN", None)
×
173
        if self.access_token:
×
174
            return True
×
175

176
        auth_url = OAuthMixin.get_auth_url(base_url, app_id)
×
177

178
        print(
×
179
            f"""I have launched a web browser with {auth_url} ,\nplease log-in,
180
             then paste the redirected URL below. \n"""
181
        )
182
        import webbrowser
×
183

184
        webbrowser.open(auth_url)
×
185
        redirect_response = input("Paste the full redirect URL here:")
×
186
        one_time_token = parse_qs(urlparse(redirect_response).query).get("ott")[0]
×
187
        print("ONE TIME TOKEN", one_time_token)
×
188

189
        self.access_token = OAuthMixin.get_user_token(
×
190
            base_url, app_id, app_token, one_time_token
191
        )
192

193
        print("ACCESS TOKEN", self.access_token)
×
194
        return True
×
195

196

197
class RequestsTransport(OAuthMixin, PBNClientTransport):
1✔
198
    def _build_headers(self, headers=None):
1✔
199
        """Build headers for API request."""
UNCOV
200
        sent_headers = {"X-App-Id": self.app_id, "X-App-Token": self.app_token}
×
UNCOV
201
        if self.access_token:
×
UNCOV
202
            sent_headers["X-User-Token"] = self.access_token
×
UNCOV
203
        if headers is not None:
×
UNCOV
204
            sent_headers.update(headers)
×
NEW
205
        return sent_headers
×
206

207
    def _make_get_request_with_retry(self, url, headers, max_retries=15):
1✔
208
        """Make GET request with retry on SSL/Connection errors."""
UNCOV
209
        retries = 0
×
NEW
210
        while retries < max_retries:
×
UNCOV
211
            try:
×
NEW
212
                return requests.get(self.base_url + url, headers=headers)
×
UNCOV
213
            except (SSLError, ConnectionError) as e:
×
UNCOV
214
                retries += 1
×
UNCOV
215
                time.sleep(random.randint(1, 5))
×
NEW
216
                if retries >= max_retries:
×
217
                    raise e
×
218

219
    def _handle_403_response(self, ret, url, headers, fail_on_auth_missing):
1✔
220
        """Handle 403 response, attempting reauthorization if needed."""
NEW
221
        if fail_on_auth_missing:
×
NEW
222
            raise AccessDeniedException(url, smart_content(ret.content))
×
223

NEW
224
        if ret.json()["message"] in ["Access Denied", "Forbidden"]:
×
NEW
225
            raise AccessDeniedException(url, smart_content(ret.content))
×
226

NEW
227
        if hasattr(self, "authorize"):
×
NEW
228
            auth_result = self.authorize(self.base_url, self.app_id, self.app_token)
×
NEW
229
            if not auth_result:
×
NEW
230
                return None
×
NEW
231
            return self.get(url, headers, fail_on_auth_missing=True)
×
232

NEW
233
        return ret
×
234

235
    def _parse_json_response(self, ret, url):
1✔
236
        """Parse JSON response with special handling for service maintenance."""
UNCOV
237
        try:
×
UNCOV
238
            return ret.json()
×
UNCOV
239
        except (RequestsJSONDecodeError, JSONDecodeError) as e:
×
UNCOV
240
            if ret.status_code == 200 and b"prace serwisowe" in ret.content:
×
NEW
241
                raise PraceSerwisoweException() from e
×
UNCOV
242
            raise e
×
243

244
    def get(self, url, headers=None, fail_on_auth_missing=False):
1✔
NEW
245
        sent_headers = self._build_headers(headers)
×
NEW
246
        ret = self._make_get_request_with_retry(url, sent_headers)
×
247

NEW
248
        if ret.status_code == 403:
×
NEW
249
            result = self._handle_403_response(ret, url, headers, fail_on_auth_missing)
×
NEW
250
            if result is None:
×
251
                return
×
NEW
252
            if result != ret:
×
NEW
253
                return result
×
254

NEW
255
        if ret.status_code >= 400:
×
NEW
256
            raise HttpException(ret.status_code, url, smart_content(ret.content))
×
257

NEW
258
        return self._parse_json_response(ret, url)
×
259

260
    def _ensure_access_token(self):
1✔
261
        """Ensure access token is available."""
NEW
262
        if not hasattr(self, "access_token"):
×
NEW
263
            return self.authorize(self.base_url, self.app_id, self.app_token)
×
NEW
264
        return True
×
265

266
    def _build_post_headers(self, headers=None):
1✔
267
        """Build headers for POST request."""
UNCOV
268
        sent_headers = {
×
269
            "X-App-Id": self.app_id,
270
            "X-App-Token": self.app_token,
271
            "X-User-Token": self.access_token,
272
        }
UNCOV
273
        if headers is not None:
×
UNCOV
274
            sent_headers.update(headers)
×
NEW
275
        return sent_headers
×
276

277
    def _get_request_method(self, delete):
1✔
278
        """Get appropriate HTTP method."""
NEW
279
        return requests.delete if delete else requests.post
×
280

281
    def _parse_403_response(self, ret, url):
1✔
282
        """Parse 403 response JSON."""
NEW
283
        try:
×
NEW
284
            return ret.json()
×
NEW
285
        except BaseException as e:
×
NEW
286
            raise HttpException(
×
287
                ret.status_code,
288
                url,
289
                "Blad podczas odkodowywania JSON podczas odpowiedzi 403: "
290
                + smart_content(ret.content),
291
            ) from e
292

293
    def _handle_403_access_denied(self, ret_json, ret, url):
1✔
294
        """Handle 403 Access Denied responses."""
NEW
295
        if ret_json.get("message") == "Access Denied":
×
NEW
296
            raise AccessDeniedException(url, smart_content(ret.content))
×
297

NEW
298
        if ret_json.get("message") == "Forbidden" and ret_json.get(
×
299
            "description", ""
300
        ).startswith(NEEDS_PBN_AUTH_MSG):
NEW
301
            raise NeedsPBNAuthorisationException(
×
302
                ret.status_code, url, smart_content(ret.content)
303
            )
304

NEW
305
        if hasattr(self, "authorize"):
×
NEW
306
            self.authorize(self.base_url, self.app_id, self.app_token)
×
307

308
    def _check_error_response(self, ret, url):
1✔
309
        """Check and handle error responses."""
310
        if ret.status_code >= 400:
×
311
            if ret.status_code == 423 and smart_content(ret.content) == "Locked":
×
312
                raise ResourceLockedException(
×
313
                    ret.status_code, url, smart_content(ret.content)
314
                )
UNCOV
315
            raise HttpException(ret.status_code, url, smart_content(ret.content))
×
316

317
    def post(self, url, headers=None, body=None, delete=False):
1✔
NEW
318
        if not self._ensure_access_token():
×
NEW
319
            return
×
NEW
320
        if not hasattr(self, "access_token"):
×
NEW
321
            return self.post(url, headers=headers, body=body, delete=delete)
×
322

NEW
323
        sent_headers = self._build_post_headers(headers)
×
NEW
324
        method = self._get_request_method(delete)
×
NEW
325
        ret = method(self.base_url + url, headers=sent_headers, json=body)
×
326

NEW
327
        if ret.status_code == 403:
×
NEW
328
            ret_json = self._parse_403_response(ret, url)
×
NEW
329
            self._handle_403_access_denied(ret_json, ret, url)
×
330

NEW
331
        self._check_error_response(ret, url)
×
332

UNCOV
333
        try:
×
UNCOV
334
            return ret.json()
×
335
        except (RequestsJSONDecodeError, JSONDecodeError) as e:
×
336
            if ret.status_code == 200:
×
337
                if ret.content == b"":
×
338
                    return
×
339

340
                if b"prace serwisowe" in ret.content:
×
341
                    # open("pbn_client_dump.html", "wb").write(ret.content)
NEW
342
                    raise PraceSerwisoweException() from e
×
343

344
            raise e
×
345

346
    def delete(
1✔
347
        self,
348
        url,
349
        headers=None,
350
        body=None,
351
    ):
UNCOV
352
        return self.post(url, headers, body, delete=True)
×
353

354
    def _pages(self, method, url, headers=None, body=None, page_size=10, *args, **kw):
1✔
355
        # Stronicowanie zwraca rezultaty w taki sposób:
356
        # {'content': [{'mongoId': '5e709189878c28a04737dc6f',
357
        #               'status': 'ACTIVE',
358
        # ...
359
        #              'versionHash': '---'}]}],
360
        #  'first': True,
361
        #  'last': False,
362
        #  'number': 0,
363
        #  'numberOfElements': 10,
364
        #  'pageable': {'offset': 0,
365
        #               'pageNumber': 0,
366
        #               'pageSize': 10,
367
        #               'paged': True,
368
        #               'sort': {'sorted': False, 'unsorted': True},
369
        #               'unpaged': False},
370
        #  'size': 10,
371
        #  'sort': {'sorted': False, 'unsorted': True},
372
        #  'totalElements': 68577,
373
        #  'totalPages': 6858}
374

UNCOV
375
        chr = "?"
×
UNCOV
376
        if url.find("?") >= 0:
×
UNCOV
377
            chr = "&"
×
378

UNCOV
379
        url = url + f"{chr}size={page_size}"
×
UNCOV
380
        chr = "&"
×
381

UNCOV
382
        for elem in kw:
×
383
            url += chr + elem + "=" + quote(kw[elem])
×
384

UNCOV
385
        method_function = getattr(self, method)
×
386

UNCOV
387
        if method == "get":
×
UNCOV
388
            res = method_function(url, headers)
×
UNCOV
389
        elif method == "post":
×
UNCOV
390
            res = method_function(url, headers, body=body)
×
391
        else:
392
            raise NotImplementedError
393

UNCOV
394
        if "pageable" not in res:
×
UNCOV
395
            warnings.warn(
×
396
                f"PBNClient.{method}_page request for {url} with headers {headers} did not return a paged resource, "
397
                f"maybe use PBNClient.{method} (without 'page') instead",
398
                RuntimeWarning,
399
                stacklevel=2,
400
            )
UNCOV
401
            return res
×
UNCOV
402
        return PageableResource(
×
403
            self, res, url=url, headers=headers, body=body, method=method
404
        )
405

406
    def get_pages(self, url, headers=None, page_size=10, *args, **kw):
1✔
UNCOV
407
        return self._pages(
×
408
            "get", *args, url=url, headers=headers, page_size=page_size, **kw
409
        )
410

411
    def post_pages(self, url, headers=None, body=None, page_size=10, *args, **kw):
1✔
412
        # Jak get_pages, ale methoda to post
UNCOV
413
        if body is None:
×
414
            body = kw
×
415

UNCOV
416
        return self._pages(
×
417
            "post",
418
            *args,
419
            url=url,
420
            headers=headers,
421
            body=body,
422
            page_size=page_size,
423
            **kw,
424
        )
425

426

427
class ConferencesMixin:
1✔
428
    def get_conferences(self, *args, **kw):
1✔
UNCOV
429
        return self.transport.get_pages("/api/v1/conferences/page", *args, **kw)
×
430

431
    def get_conferences_mnisw(self, *args, **kw):
1✔
432
        return self.transport.get_pages("/api/v1/conferences/mnisw/page", *args, **kw)
×
433

434
    def get_conference(self, id):
1✔
435
        return self.transport.get(f"/api/v1/conferences/{id}")
×
436

437
    def get_conference_editions(self, id):
1✔
438
        return self.transport.get(f"/api/v1/conferences/{id}/editions")
×
439

440
    def get_conference_metadata(self, id):
1✔
441
        return self.transport.get(f"/api/v1/conferences/{id}/metadata")
×
442

443

444
class DictionariesMixin:
1✔
445
    def get_countries(self):
1✔
446
        return self.transport.get("/api/v1/dictionary/countries")
×
447
        return self.transport.get("/api/v1/dictionary/countries")
448

449
    def get_disciplines(self):
1✔
UNCOV
450
        return self.transport.get(PBN_GET_DISCIPLINES_URL)
×
451

452
    def get_languages(self):
1✔
453
        return self.transport.get(PBN_GET_LANGUAGES_URL)
×
454

455

456
class InstitutionsMixin:
1✔
457
    def get_institutions(self, status="ACTIVE", *args, **kw):
1✔
458
        return self.transport.get_pages(
×
459
            "/api/v1/institutions/page", *args, status=status, **kw
460
        )
461

462
    def get_institution_by_id(self, id):
1✔
463
        return self.transport.get(f"/api/v1/institutions/{id}")
×
464

465
    def get_institution_by_version(self, version):
1✔
466
        return self.transport.get_pages(f"/api/v1/institutions/version/{version}")
×
467

468
    def get_institution_metadata(self, id):
1✔
469
        return self.transport.get_pages(f"/api/v1/institutions/{id}/metadata")
×
470

471
    def get_institutions_polon(self, includeAllVersions="true", *args, **kw):
1✔
472
        return self.transport.get_pages(
×
473
            "/api/v1/institutions/polon/page",
474
            *args,
475
            includeAllVersions=includeAllVersions,
476
            **kw,
477
        )
478

479
    def get_institutions_polon_by_uid(self, uid):
1✔
480
        return self.transport.get(f"/api/v1/institutions/polon/uid/{uid}")
×
481

482
    def get_institutions_polon_by_id(self, id):
1✔
483
        return self.transport.get(f"/api/v1/institutions/polon/{id}")
×
484

485

486
class InstitutionsProfileMixin:
1✔
487
    def get_institution_publications(self, page_size=10) -> PageableResource:
1✔
488
        return self.transport.get_pages(
×
489
            "/api/v1/institutionProfile/publications/page", page_size=page_size
490
        )
491

492
    def get_institution_publications_v2(
1✔
493
        self,
494
    ) -> PageableResource:
495
        return self.transport.get_pages(PBN_GET_INSTITUTION_PUBLICATIONS_V2)
×
496

497
    def get_institution_statements(self, page_size=10):
1✔
498
        return self.transport.get_pages(
×
499
            PBN_GET_INSTITUTION_STATEMENTS,
500
            page_size=page_size,
501
        )
502

503
    def get_institution_statements_of_single_publication(
1✔
504
        self, pbn_uid_id, page_size=50
505
    ):
UNCOV
506
        return self.transport.get_pages(
×
507
            PBN_GET_INSTITUTION_STATEMENTS + "?publicationId=" + pbn_uid_id,
508
            page_size=page_size,
509
        )
510

511
    def get_institution_publication_v2(
1✔
512
        self,
513
        objectId,
514
    ):
UNCOV
515
        return self.transport.get_pages(
×
516
            PBN_GET_INSTITUTION_PUBLICATIONS_V2 + f"?publicationId={objectId}",
517
        )
518

519
    def delete_all_publication_statements(self, publicationId):
1✔
UNCOV
520
        url = PBN_DELETE_PUBLICATION_STATEMENT.format(publicationId=publicationId)
×
UNCOV
521
        try:
×
UNCOV
522
            return self.transport.delete(
×
523
                url,
524
                body={"all": True, "statementsOfPersons": []},
525
            )
UNCOV
526
        except HttpException as e:
×
UNCOV
527
            if e.status_code != 400 or not e.url.startswith(url):
×
528
                raise e
×
529

UNCOV
530
            try:
×
UNCOV
531
                ret_json = json.loads(e.content)
×
NEW
532
            except BaseException as parse_err:
×
NEW
533
                raise e from parse_err
×
NEW
534
            ZABLOKOWANE = "zostało tymczasowo zablokowane z uwagi na równoległą operację. Prosimy spróbować ponownie."
×
UNCOV
535
            NIE_MOZNA_USUNAC = "Nie można usunąć oświadczeń."
×
UNCOV
536
            NIE_ISTNIEJA = "Nie istnieją oświadczenia dla publikacji"
×
UNCOV
537
            NIE_ISTNIEJE = "Nie istnieje oświadczenie dla publikacji"
×
538

UNCOV
539
            if ret_json:
×
NEW
540
                if e.json.get("message") == "Locked" and ZABLOKOWANE in e.content:
×
NEW
541
                    raise ResourceLockedException(e.content) from e
×
542

UNCOV
543
                try:
×
UNCOV
544
                    try:
×
UNCOV
545
                        msg = e.json["details"]["publicationId"]
×
546
                    except KeyError:
×
547
                        msg = e.json["details"][f"publicationId.{publicationId}"]
×
548

UNCOV
549
                    if (
×
550
                        NIE_ISTNIEJA in msg or NIE_ISTNIEJE in msg
551
                    ) and NIE_MOZNA_USUNAC in msg:
552
                        # Opis odpowiada sytuacji "Nie można usunąć oświadczeń, nie istnieją"
UNCOV
553
                        raise CannotDeleteStatementsException(e.content)
×
554

NEW
555
                except (TypeError, KeyError) as key_err:
×
556
                    if (
×
557
                        NIE_ISTNIEJA in e.content or NIE_ISTNIEJE in e.content
558
                    ) and NIE_MOZNA_USUNAC in e.content:
NEW
559
                        raise CannotDeleteStatementsException(e.content) from key_err
×
560

561
            raise e
×
562

563
    def delete_publication_statement(self, publicationId, personId, role):
1✔
UNCOV
564
        return self.transport.delete(
×
565
            PBN_DELETE_PUBLICATION_STATEMENT.format(publicationId=publicationId),
566
            body={"statementsOfPersons": [{"personId": personId, "role": role}]},
567
        )
568

569
    def post_discipline_statements(self, statements_data):
1✔
570
        """
571
        Send discipline statements to PBN API.
572

573
        Args:
574
            statements_data (list): List of statement dictionaries containing discipline information
575

576
        Returns:
577
            dict: Response from PBN API
578
        """
579
        return self.transport.post(
×
580
            PBN_POST_INSTITUTION_STATEMENTS_URL, body=statements_data
581
        )
582

583

584
class JournalsMixin:
1✔
585
    def get_journals_mnisw(self, *args, **kw):
1✔
586
        return self.transport.get_pages("/api/v1/journals/mnisw/page", *args, **kw)
×
587

588
    def get_journals_mnisw_v2(self, *args, **kw):
1✔
589
        return self.transport.get_pages("/api/v2/journals/mnisw/page", *args, **kw)
×
590

591
    def get_journals(self, *args, **kw):
1✔
592
        return self.transport.get_pages("/api/v1/journals/page", *args, **kw)
×
593

594
    def get_journals_v2(self, *args, **kw):
1✔
595
        return self.transport.get_pages("/api/v2/journals/page", *args, **kw)
×
596

597
    def get_journal_by_version(self, version):
1✔
598
        return self.transport.get(f"/api/v1/journals/version/{version}")
×
599

600
    def get_journal_by_id(self, id):
1✔
UNCOV
601
        return self.transport.get(PBN_GET_JOURNAL_BY_ID.format(id=id))
×
602

603
    def get_journal_metadata(self, id):
1✔
604
        return self.transport.get(f"/api/v1/journals/{id}/metadata")
×
605

606

607
class PersonMixin:
1✔
608
    def get_people_by_institution_id(self, id):
1✔
609
        return self.transport.get(f"/api/v1/person/institution/{id}")
×
610

611
    def get_person_by_natural_id(self, id):
1✔
612
        return self.transport.get(f"/api/v1/person/natural/{id}")
×
613

614
    def get_person_by_orcid(self, orcid):
1✔
615
        return self.transport.get(f"/api/v1/person/orcid/{orcid}")
×
616

617
    def get_people(self, *args, **kw):
1✔
618
        return self.transport.get_pages("/api/v1/person/page", *args, **kw)
×
619

620
    def get_person_by_polon_uid(self, uid):
1✔
621
        return self.transport.get(f"/api/v1/person/polon/{uid}")
×
622

623
    def get_person_by_version(self, version):
1✔
624
        return self.transport.get(f"/api/v1/person/version/{version}")
×
625

626
    def get_person_by_id(self, id):
1✔
627
        return self.transport.get(f"/api/v1/person/{id}")
×
628

629

630
class PublishersMixin:
1✔
631
    def get_publishers_mnisw(self, *args, **kw):
1✔
632
        return self.transport.get_pages("/api/v1/publishers/mnisw/page", *args, **kw)
×
633

634
    def get_publishers_mnisw_yearlist(self, *args, **kw):
1✔
635
        return self.transport.get_pages(
×
636
            "/api/v1/publishers/mnisw/page/yearlist", *args, **kw
637
        )
638

639
    def get_publishers(self, *args, **kw):
1✔
640
        return self.transport.get_pages("/api/v1/publishers/page", *args, **kw)
×
641

642
    def get_publisher_by_version(self, version):
1✔
643
        return self.transport.get(f"/api/v1/publishers/version/{version}")
×
644

645
    def get_publisher_by_id(self, id):
1✔
646
        return self.transport.get(f"/api/v1/publishers/{id}")
×
647

648
    def get_publisher_metadata(self, id):
1✔
649
        return self.transport.get(f"/api/v1/publishers/{id}/metadata")
×
650

651

652
class PublicationsMixin:
1✔
653
    def get_publication_by_doi(self, doi):
1✔
654
        return self.transport.get(
×
655
            f"/api/v1/publications/doi/?doi={quote(doi, safe='')}",
656
        )
657

658
    def get_publication_by_doi_page(self, doi):
1✔
659
        return self.transport.get_pages(
×
660
            f"/api/v1/publications/doi/page?doi={quote(doi, safe='')}",
661
            headers={"doi": doi},
662
        )
663

664
    def get_publication_by_id(self, id):
1✔
UNCOV
665
        return self.transport.get(PBN_GET_PUBLICATION_BY_ID_URL.format(id=id))
×
666

667
    def get_publication_metadata(self, id):
1✔
668
        return self.transport.get(f"/api/v1/publications/id/{id}/metadata")
×
669

670
    def get_publications(self, **kw):
1✔
671
        return self.transport.get_pages("/api/v1/publications/page", **kw)
×
672

673
    def get_publication_by_version(self, version):
1✔
674
        return self.transport.get(f"/api/v1/publications/version/{version}")
×
675

676

677
class SearchMixin:
1✔
678
    def search_publications(self, *args, **kw):
1✔
UNCOV
679
        return self.transport.post_pages(PBN_SEARCH_PUBLICATIONS_URL, body=kw)
×
680

681

682
class PBNClient(
1✔
683
    ConferencesMixin,
684
    DictionariesMixin,
685
    InstitutionsMixin,
686
    InstitutionsProfileMixin,
687
    JournalsMixin,
688
    PersonMixin,
689
    PublicationsMixin,
690
    PublishersMixin,
691
    SearchMixin,
692
):
693
    _interactive = False
1✔
694

695
    def __init__(self, transport: RequestsTransport):
1✔
696
        self.transport = transport
1✔
697

698
    def post_publication(self, json):
1✔
UNCOV
699
        return self.transport.post(PBN_POST_PUBLICATIONS_URL, body=json)
×
700

701
    def convert_js_with_statements_to_no_statements(self, json):
1✔
702
        # PBN zmienił givenNames na firstName
UNCOV
703
        for elem in json.get("authors", []):
×
UNCOV
704
            elem["firstName"] = elem.pop("givenNames")
×
705

UNCOV
706
        for elem in json.get("editors", []):
×
707
            elem["firstName"] = elem.pop("givenNames")
×
708

709
        # PBN życzy abstrakty w root
UNCOV
710
        abstracts = json.pop("languageData", {}).get("abstracts", [])
×
UNCOV
711
        if abstracts:
×
712
            json["abstracts"] = abstracts
×
713

714
        # PBN nie życzy opłat
UNCOV
715
        json.pop("fee", None)
×
716

717
        # PBN zmienił nazwę mniswId na ministryId
UNCOV
718
        json = rename_dict_key(json, "mniswId", "ministryId")
×
719

720
        # OpenAccess modeArticle -> mode
UNCOV
721
        json = rename_dict_key(json, "modeArticle", "mode")
×
722

723
        # OpenAccess releaseDateYear "2022" -> 2022
UNCOV
724
        if json.get("openAccess", False):
×
725
            if isinstance(json["openAccess"], dict) and json["openAccess"].get(
×
726
                "releaseDateYear"
727
            ):
728
                try:
×
729
                    i = int(json["openAccess"]["releaseDateYear"])
×
730
                except (ValueError, TypeError, AttributeError):
×
731
                    pass
×
732

733
                json["openAccess"]["releaseDateYear"] = i
×
UNCOV
734
        return json
×
735

736
    def post_publication_no_statements(self, json):
1✔
737
        """
738
        Ta funkcja służy do wysyłania publikacji BEZ oświadczeń.
739

740
        Bierzemy słownik JSON z publikacji-z-oświadczeniami i przetwarzamy go.
741

742
        :param json:
743
        :return:
744
        """
UNCOV
745
        return self.transport.post(PBN_POST_PUBLICATION_NO_STATEMENTS_URL, body=[json])
×
746

747
    def post_publication_fee(self, publicationId, json):
1✔
748
        return self.transport.post(
×
749
            PBN_POST_PUBLICATION_FEE_URL.format(id=publicationId), body=json
750
        )
751

752
    def get_publication_fee(self, publicationId):
1✔
753
        res = self.transport.post_pages(
×
754
            "/api/v1/institutionProfile/publications/search/fees",
755
            body={"publicationIds": [str(publicationId)]},
756
        )
757
        if not res.count():
×
758
            return
×
759
        elif res.count() == 1:
×
760
            return list(res)[0]
×
761
        else:
762
            raise NotImplementedError("count > 1")
763

764
    def _prepare_publication_json(self, rec, export_pk_zero, always_affiliate_to_uid):
1✔
765
        """Prepare publication JSON data."""
NEW
766
        js = WydawnictwoPBNAdapter(
×
767
            rec,
768
            export_pk_zero=export_pk_zero,
769
            always_affiliate_to_uid=always_affiliate_to_uid,
770
        ).pbn_get_json()
771

NEW
772
        bez_oswiadczen = "statements" not in js
×
NEW
773
        if bez_oswiadczen:
×
NEW
774
            js = self.convert_js_with_statements_to_no_statements(js)
×
775

NEW
776
        return js, bez_oswiadczen
×
777

778
    def _check_upload_needed(self, rec, js, force_upload):
1✔
779
        """Check if upload is needed."""
NEW
780
        if not force_upload:
×
NEW
781
            needed = SentData.objects.check_if_upload_needed(rec, js)
×
NEW
782
            if not needed:
×
NEW
783
                raise SameDataUploadedRecently(
×
784
                    SentData.objects.get_for_rec(rec).last_updated_on
785
                )
786

787
    def _post_publication_data(self, js, bez_oswiadczen):
1✔
788
        """Post publication data and extract objectId."""
NEW
789
        if not bez_oswiadczen:
×
NEW
790
            ret = self.post_publication(js)
×
NEW
791
            objectId = ret.get("objectId", None)
×
792
        else:
NEW
793
            ret = self.post_publication_no_statements(js)
×
NEW
794
            if len(ret) != 1:
×
NEW
795
                raise Exception(
×
796
                    "Lista zwróconych obiektów przy wysyłce pracy bez oświadczeń różna od jednego. "
797
                    "Sytuacja nieobsługiwana, proszę o kontakt z autorem programu. "
798
                )
NEW
799
            try:
×
NEW
800
                objectId = ret[0].get("id", None)
×
NEW
801
            except KeyError as e:
×
NEW
802
                raise Exception(
×
803
                    f"Serwer zwrócił nieoczekiwaną odpowiedź. {ret=}"
804
                ) from e
805

NEW
806
        return ret, objectId
×
807

808
    def _should_retry_validation_error(self, e):
1✔
809
        """Check if HTTP exception is a retryable validation error."""
NEW
810
        return (
×
811
            e.status_code == 400
812
            and e.url == "/api/v1/publications"
813
            and "Bad Request" in e.content
814
            and "Validation failed." in e.content
815
        )
816

817
    def _retry_download_publication(self, objectId):
1✔
818
        """Attempt to download publication data after validation error."""
NEW
819
        try:
×
NEW
820
            publication = self.download_publication(objectId=objectId)
×
NEW
821
            self.download_statements_of_publication(publication)
×
NEW
822
            self.pobierz_publikacje_instytucji_v2(objectId=objectId)
×
NEW
823
        except Exception:
×
NEW
824
            pass
×
825

826
    def upload_publication(
1✔
827
        self,
828
        rec,
829
        force_upload=False,
830
        export_pk_zero=None,
831
        always_affiliate_to_uid=None,
832
        max_retries_on_validation_error=3,
833
    ):
834
        """
835
        Ta funkcja wysyła dane publikacji na serwer, w zależności od obecności oświadczeń
836
        w JSONie (klucz: "statements") używa albo api /v1/ do wysyłki publikacji "ze wszystkim",
837
        albo korzysta z api /v1/ repozytorialnego.
838

839
        Zwracane wyniki wyjściowe też różnią się w zależnosci od użytego API stąd też ta funkcja
840
        stara się w miarę rozsądnie to ogarnąć.
841
        """
NEW
842
        js, bez_oswiadczen = self._prepare_publication_json(
×
843
            rec, export_pk_zero, always_affiliate_to_uid
844
        )
NEW
845
        self._check_upload_needed(rec, js, force_upload)
×
846

847
        # Create or update SentData record BEFORE API call
UNCOV
848
        sent_data = SentData.objects.create_or_update_before_upload(rec, js)  # noqa
×
849

UNCOV
850
        retry_count = max_retries_on_validation_error
×
UNCOV
851
        ret = None
×
UNCOV
852
        objectId = None
×
853

UNCOV
854
        while True:
×
UNCOV
855
            try:
×
NEW
856
                ret, objectId = self._post_publication_data(js, bez_oswiadczen)
×
UNCOV
857
                SentData.objects.mark_as_successful(
×
858
                    rec, pbn_uid_id=objectId, api_response_status=str(ret)
859
                )
UNCOV
860
                break
×
861

UNCOV
862
            except HttpException as e:
×
NEW
863
                if self._should_retry_validation_error(e):
×
UNCOV
864
                    retry_count -= 1
×
865
                    if retry_count <= 0:
×
UNCOV
866
                        SentData.objects.mark_as_failed(
×
867
                            rec, exception=str(e), api_response_status=e.content
868
                        )
869
                        raise e
×
870

871
                    time.sleep(0.5)
×
NEW
872
                    self._retry_download_publication(objectId)
×
UNCOV
873
                    continue
×
874

UNCOV
875
                SentData.objects.mark_as_failed(
×
876
                    rec, exception=str(e), api_response_status=e.content
877
                )
878
                raise e
×
879

UNCOV
880
            except Exception as e:
×
UNCOV
881
                SentData.objects.mark_as_failed(rec, exception=str(e))
×
UNCOV
882
                raise e
×
883

UNCOV
884
        return objectId, ret, js, bez_oswiadczen
×
885

886
    def download_publication(self, doi=None, objectId=None):
1✔
UNCOV
887
        from pbn_integrator.utils import zapisz_mongodb
×
888

UNCOV
889
        from .models import Publication
×
890

UNCOV
891
        assert doi or objectId
×
892

UNCOV
893
        if doi:
×
894
            data = self.get_publication_by_doi(doi)
×
UNCOV
895
        elif objectId:
×
UNCOV
896
            data = self.get_publication_by_id(objectId)
×
897

UNCOV
898
        return zapisz_mongodb(data, Publication)
×
899

900
    @transaction.atomic
1✔
901
    def download_statements_of_publication(self, pub):
1✔
UNCOV
902
        from pbn_api.models import OswiadczenieInstytucji
×
UNCOV
903
        from pbn_integrator.utils import pobierz_mongodb, zapisz_oswiadczenie_instytucji
×
904

UNCOV
905
        OswiadczenieInstytucji.objects.filter(publicationId_id=pub.pk).delete()
×
906

UNCOV
907
        pobierz_mongodb(
×
908
            self.get_institution_statements_of_single_publication(pub.pk, 5120),
909
            None,
910
            fun=zapisz_oswiadczenie_instytucji,
911
            client=self,
912
            disable_progress_bar=True,
913
        )
914

915
    def pobierz_publikacje_instytucji_v2(self, objectId):
1✔
UNCOV
916
        from pbn_integrator.utils import zapisz_publikacje_instytucji_v2
×
917

UNCOV
918
        elem = list(self.get_institution_publication_v2(objectId=objectId))
×
UNCOV
919
        if not elem:
×
920
            raise PublikacjaInstytucjiV2NieZnalezionaException(objectId)
×
921

UNCOV
922
        if len(elem) != 1:
×
923
            raise ZnalezionoWielePublikacjiInstytucjiV2Exception(objectId)
×
924

UNCOV
925
        return zapisz_publikacje_instytucji_v2(self, elem[0])
×
926

927
    def _delete_statements_with_retry(self, pbn_uid_id, max_tries=5):
1✔
928
        """Delete publication statements with retry on failure."""
NEW
929
        no_tries = max_tries
×
NEW
930
        while True:
×
NEW
931
            try:
×
NEW
932
                self.delete_all_publication_statements(pbn_uid_id)
×
NEW
933
                return True
×
NEW
934
            except CannotDeleteStatementsException as e:
×
NEW
935
                if no_tries < 0:
×
NEW
936
                    raise e
×
NEW
937
                no_tries -= 1
×
NEW
938
                time.sleep(0.5)
×
939

940
    def _handle_no_objectid(self, notificator, ret, js, pub):
1✔
941
        """Handle case when server doesn't return object ID."""
NEW
942
        msg = (
×
943
            f"UWAGA. Serwer PBN nie odpowiedział prawidłowym PBN UID dla"
944
            f" wysyłanego rekordu. Zgłoś sytuację do administratora serwisu. "
945
            f"{ret=}, {js=}, {pub=}"
946
        )
NEW
947
        if notificator is not None:
×
NEW
948
            notificator.error(msg)
×
949

NEW
950
        try:
×
NEW
951
            raise NoPBNUIDException(msg)
×
NEW
952
        except NoPBNUIDException:
×
NEW
953
            rollbar.report_exc_info(sys.exc_info())
×
954

NEW
955
        mail_admins("Serwer PBN nie zwrocil ID publikacji", msg, fail_silently=True)
×
956

957
    def _download_statements_with_retry(
1✔
958
        self, publication, objectId, notificator, max_tries=3
959
    ):
960
        """Download publication statements with retry on 500 errors."""
NEW
961
        no_tries = max_tries
×
NEW
962
        while True:
×
NEW
963
            try:
×
NEW
964
                self.download_statements_of_publication(publication)
×
NEW
965
                break
×
NEW
966
            except HttpException as e:
×
NEW
967
                if no_tries < 0 or e.status_code != 500:
×
NEW
968
                    raise e
×
NEW
969
                no_tries -= 1
×
NEW
970
                time.sleep(0.5)
×
971

NEW
972
        try:
×
NEW
973
            self.pobierz_publikacje_instytucji_v2(objectId=objectId)
×
NEW
974
        except PublikacjaInstytucjiV2NieZnalezionaException:
×
NEW
975
            notificator.warning(
×
976
                "Nie znaleziono oświadczeń dla publikacji po stronie PBN w wersji V2 API. Ten komunikat nie jest "
977
                "błędem. "
978
            )
979

980
    def _get_username_from_notificator(self, notificator):
1✔
981
        """Extract username from notificator if available."""
NEW
982
        if (
×
983
            notificator is not None
984
            and hasattr(notificator, "request")
985
            and hasattr(notificator.request, "user")
986
        ):
NEW
987
            return notificator.request.user.username
×
NEW
988
        return None
×
989

990
    def _handle_uid_change(self, pub, objectId, notificator, js, ret):
1✔
991
        """Handle case when publication UID changes."""
NEW
992
        if notificator is not None:
×
NEW
993
            notificator.error(
×
994
                f"UWAGA UWAGA UWAGA. Wg danych z PBN zmodyfikowano PBN UID tego rekordu "
995
                f"z wartości {pub.pbn_uid_id} na {objectId}. Technicznie nie jest to błąd, "
996
                f"ale w praktyce dobrze by było zweryfikować co się zadziało, zarówno po stronie"
997
                f"PBNu jak i BPP. Być może operujesz na rekordzie ze zdublowanym DOI/stronie WWW."
998
            )
999

NEW
1000
        message = (
×
1001
            f"Zarejestrowano zmianę ZAPISANEGO WCZEŚNIEJ PBN UID publikacji przez PBN, \n"
1002
            f"Publikacja:\n{pub}\n\n"
1003
            f"z UIDu {pub.pbn_uid_id} na {objectId}"
1004
        )
1005

NEW
1006
        try:
×
NEW
1007
            raise PBNUIDChangedException(message)
×
NEW
1008
        except PBNUIDChangedException:
×
NEW
1009
            rollbar.report_exc_info(sys.exc_info())
×
1010

NEW
1011
        mail_admins(
×
1012
            "Zmiana PBN UID publikacji przez serwer PBN", message, fail_silently=True
1013
        )
1014

NEW
1015
        PBNOdpowiedziNiepozadane.objects.create(
×
1016
            rekord=pub,
1017
            dane_wyslane=js,
1018
            odpowiedz_serwera=ret,
1019
            rodzaj_zdarzenia=PBNOdpowiedziNiepozadane.ZMIANA_UID,
1020
            uzytkownik=self._get_username_from_notificator(notificator),
1021
            stary_uid=pub.pbn_uid_id,
1022
            nowy_uid=objectId,
1023
        )
1024

1025
    def _handle_uid_conflict(self, pub, objectId, notificator, js, ret):
1✔
1026
        """Handle case when new publication gets an existing UID."""
NEW
1027
        from bpp.models import Rekord
×
1028

NEW
1029
        istniejace_rekordy = Rekord.objects.filter(pbn_uid_id=objectId)
×
NEW
1030
        if notificator is not None:
×
NEW
1031
            notificator.error(
×
1032
                f'UWAGA UWAGA UWAGA. Wysłany rekord "{pub}" dostał w odpowiedzi z serwera PBN numer UID '
1033
                f"rekordu JUŻ ISTNIEJĄCEGO W BAZIE DANYCH BPP, a konkretnie {istniejace_rekordy.all()}. "
1034
                f"Z przyczyn oczywistych NIE MOGĘ ustawić takiego PBN UID gdyż wówczas unikalność numerów PBN "
1035
                f"UID byłaby naruszona. Zapewne też doszło do "
1036
                f"NADPISANIA danych w/wym rekordu po stronie PBNu. Powinieneś/aś wycofać zmiany w PBNie "
1037
                f"za pomocą GUI, zgłosić tą sytuację do administratora oraz zaprzestać prób wysyłki "
1038
                f"tego rekordu do wyjaśnienia. "
1039
            )
1040

NEW
1041
        message = (
×
1042
            f"Zarejestrowano ustawienie nowo wysłanej pracy ISTNIEJĄCEGO JUŻ W BAZIE PBN UID\n"
1043
            f"Publikacja:\n{pub}\n\n"
1044
            f"UIDu {objectId}\n"
1045
            f"Istniejąca praca/e: {istniejace_rekordy.all()}"
1046
        )
1047

NEW
1048
        try:
×
NEW
1049
            raise PBNUIDSetToExistentException(message)
×
NEW
1050
        except PBNUIDSetToExistentException:
×
NEW
1051
            rollbar.report_exc_info(sys.exc_info())
×
1052

NEW
1053
        mail_admins(
×
1054
            "Ustawienie ISTNIEJĄCEGO JUŻ W BAZIE PBN UID publikacji przez serwer PBN",
1055
            message,
1056
            fail_silently=True,
1057
        )
1058

NEW
1059
        PBNOdpowiedziNiepozadane.objects.create(
×
1060
            rekord=pub,
1061
            dane_wyslane=js,
1062
            odpowiedz_serwera=ret,
1063
            rodzaj_zdarzenia=PBNOdpowiedziNiepozadane.UID_JUZ_ISTNIEJE,
1064
            uzytkownik=self._get_username_from_notificator(notificator),
1065
            nowy_uid=objectId,
1066
        )
1067

1068
    def sync_publication(
1✔
1069
        self,
1070
        pub,
1071
        notificator=None,
1072
        force_upload=False,
1073
        delete_statements_before_upload=False,
1074
        export_pk_zero=None,
1075
        always_affiliate_to_uid=None,
1076
    ):
1077
        """
1078
        @param delete_statements_before_upload: gdy True, kasuj oświadczenia publikacji przed wysłaniem (jeżeli posiada
1079
        PBN UID)
1080
        """
UNCOV
1081
        pub = self.eventually_coerce_to_publication(pub)
×
1082

UNCOV
1083
        if (
×
1084
            delete_statements_before_upload
1085
            and hasattr(pub, "pbn_uid_id")
1086
            and pub.pbn_uid_id is not None
1087
        ):
UNCOV
1088
            try:
×
NEW
1089
                self._delete_statements_with_retry(pub.pbn_uid_id)
×
UNCOV
1090
                force_upload = True
×
UNCOV
1091
            except CannotDeleteStatementsException:
×
UNCOV
1092
                pass
×
1093

UNCOV
1094
        objectId, ret, js, bez_oswiadczen = self.upload_publication(
×
1095
            pub,
1096
            force_upload=force_upload,
1097
            export_pk_zero=export_pk_zero,
1098
            always_affiliate_to_uid=always_affiliate_to_uid,
1099
        )
1100

NEW
1101
        if bez_oswiadczen and notificator is not None:
×
NEW
1102
            notificator.info(
×
1103
                "Rekord nie posiada oświadczeń - wysłano wyłącznie do repozytorium PBN. "
1104
            )
1105

NEW
1106
        if not objectId:
×
NEW
1107
            self._handle_no_objectid(notificator, ret, js, pub)
×
UNCOV
1108
            return
×
1109

UNCOV
1110
        publication = self.download_publication(objectId=objectId)
×
1111

UNCOV
1112
        if not bez_oswiadczen:
×
NEW
1113
            self._download_statements_with_retry(publication, objectId, notificator)
×
1114

UNCOV
1115
        if pub.pbn_uid_id != objectId:
×
UNCOV
1116
            if pub.pbn_uid_id is not None:
×
NEW
1117
                self._handle_uid_change(pub, objectId, notificator, js, ret)
×
1118

UNCOV
1119
            from bpp.models import Rekord
×
1120

UNCOV
1121
            istniejace_rekordy = Rekord.objects.filter(pbn_uid_id=objectId)
×
UNCOV
1122
            if pub.pbn_uid_id is None and istniejace_rekordy.exists():
×
NEW
1123
                self._handle_uid_conflict(pub, objectId, notificator, js, ret)
×
UNCOV
1124
                return
×
1125

UNCOV
1126
            pub.pbn_uid = publication
×
UNCOV
1127
            pub.save()
×
1128

UNCOV
1129
        return publication
×
1130

1131
    def eventually_coerce_to_publication(self, pub: Model | str) -> Model:
1✔
UNCOV
1132
        if type(pub) is str:
×
1133
            # Ciag znaków w postaci wydawnictwo_zwarte:123 pozwoli na podawanie tego
1134
            # parametru do wywołań z linii poleceń
UNCOV
1135
            model, pk = pub.split(":")
×
UNCOV
1136
            ctype = ContentType.objects.get(app_label="bpp", model=model)
×
UNCOV
1137
            pub = ctype.model_class().objects.get(pk=pk)
×
1138

UNCOV
1139
        return pub
×
1140

1141
    def upload_publication_fee(self, pub: Model):
1✔
1142
        pub = self.eventually_coerce_to_publication(pub)
×
1143
        if pub.pbn_uid_id is None:
×
1144
            raise NoPBNUIDException(
×
1145
                f"PBN UID (czyli 'numer odpowiednika w PBN') dla rekordu '{pub}' jest pusty."
1146
            )
1147

1148
        fee = OplataZaWydawnictwoPBNAdapter(pub).pbn_get_json()
×
1149
        if not fee:
×
1150
            raise NoFeeDataException(
×
1151
                f"Brak danych o opłatach za publikację {pub.pbn_uid_id}"
1152
            )
1153

1154
        return self.post_publication_fee(pub.pbn_uid_id, fee)
×
1155

1156
    def _get_command_function(self, cmd):
1✔
1157
        """Get function to execute from command name."""
1158
        try:
×
NEW
1159
            return getattr(self, cmd[0])
×
1160
        except AttributeError as e:
×
1161
            if self._interactive:
×
NEW
1162
                print(f"No such command: {cmd}")
×
NEW
1163
                return None
×
NEW
1164
            raise e
×
1165

1166
    def _extract_arguments(self, lst):
1✔
1167
        """Extract positional and keyword arguments from command list."""
NEW
1168
        args = ()
×
NEW
1169
        kw = {}
×
NEW
1170
        for elem in lst:
×
NEW
1171
            if elem.find(":") >= 1:
×
NEW
1172
                k, n = elem.split(":", 1)
×
NEW
1173
                kw[k] = n
×
1174
            else:
NEW
1175
                args += (elem,)
×
NEW
1176
        return args, kw
×
1177

1178
    def _print_non_interactive_result(self, res):
1✔
1179
        """Print result in non-interactive mode."""
NEW
1180
        import json
×
1181

NEW
1182
        print(json.dumps(res))
×
1183

1184
    def _print_interactive_result(self, res):
1✔
1185
        """Print result in interactive mode."""
NEW
1186
        if type(res) is dict:
×
NEW
1187
            pprint(res)
×
NEW
1188
        elif is_iterable(res):
×
NEW
1189
            if self._interactive and hasattr(res, "total_elements"):
×
NEW
1190
                print(
×
1191
                    "Incoming data: no_elements=",
1192
                    res.total_elements,
1193
                    "no_pages=",
1194
                    res.total_pages,
1195
                )
NEW
1196
                input("Press ENTER to continue> ")
×
NEW
1197
            for elem in res:
×
NEW
1198
                pprint(elem)
×
1199

1200
    def exec(self, cmd):
1✔
NEW
1201
        fun = self._get_command_function(cmd)
×
NEW
1202
        if fun is None:
×
NEW
1203
            return
×
1204

NEW
1205
        args, kw = self._extract_arguments(cmd[1:])
×
1206
        res = fun(*args, **kw)
×
1207

1208
        if not sys.stdout.isatty():
×
NEW
1209
            self._print_non_interactive_result(res)
×
1210
        else:
NEW
1211
            self._print_interactive_result(res)
×
1212

1213
    @transaction.atomic
1✔
1214
    def download_disciplines(self):
1✔
1215
        """Zapisuje słownik dyscyplin z API PBN do lokalnej bazy"""
1216

UNCOV
1217
        for elem in self.get_disciplines():
×
UNCOV
1218
            validityDateFrom = elem.get("validityDateFrom", None)
×
UNCOV
1219
            validityDateTo = elem.get("validityDateTo", None)
×
UNCOV
1220
            uuid = elem["uuid"]
×
1221

UNCOV
1222
            parent_group, created = DisciplineGroup.objects.update_or_create(
×
1223
                uuid=uuid,
1224
                defaults={
1225
                    "validityDateFrom": validityDateFrom,
1226
                    "validityDateTo": validityDateTo,
1227
                },
1228
            )
1229

UNCOV
1230
            for discipline in elem["disciplines"]:
×
1231
                # print("XXX", discipline["uuid"])
UNCOV
1232
                Discipline.objects.update_or_create(
×
1233
                    parent_group=parent_group,
1234
                    uuid=discipline["uuid"],
1235
                    defaults=dict(
1236
                        code=discipline["code"],
1237
                        name=discipline["name"],
1238
                        polonCode=discipline["polonCode"],
1239
                        scientificFieldName=discipline["scientificFieldName"],
1240
                    ),
1241
                )
1242

1243
    @transaction.atomic
1✔
1244
    def sync_disciplines(self):
1✔
UNCOV
1245
        self.download_disciplines()
×
UNCOV
1246
        try:
×
UNCOV
1247
            cur_dg = DisciplineGroup.objects.get_current()
×
NEW
1248
        except DisciplineGroup.DoesNotExist as e:
×
1249
            raise ValueError(
×
1250
                "Brak aktualnego słownika dyscyplin na serwerze. Pobierz aktualny słownik "
1251
                "dyscyplin z PBN."
1252
            ) from e
1253

UNCOV
1254
        from bpp.models import Dyscyplina_Naukowa
×
1255

UNCOV
1256
        for dyscyplina in Dyscyplina_Naukowa.objects.all():
×
UNCOV
1257
            wpis_tlumacza = TlumaczDyscyplin.objects.get_or_create(
×
1258
                dyscyplina_w_bpp=dyscyplina
1259
            )[0]
1260

UNCOV
1261
            wpis_tlumacza.pbn_2024_now = matchuj_aktualna_dyscypline_pbn(
×
1262
                dyscyplina.kod, dyscyplina.nazwa
1263
            )
1264
            # Domyślnie szuka dla lat 2018-2022
UNCOV
1265
            wpis_tlumacza.pbn_2017_2021 = matchuj_nieaktualna_dyscypline_pbn(
×
1266
                dyscyplina.kod, dyscyplina.nazwa, rok_min=2018, rok_max=2022
1267
            )
1268

UNCOV
1269
            wpis_tlumacza.pbn_2022_2023 = matchuj_nieaktualna_dyscypline_pbn(
×
1270
                dyscyplina.kod, dyscyplina.nazwa, rok_min=2023, rok_max=2024
1271
            )
1272

UNCOV
1273
            wpis_tlumacza.save()
×
1274

UNCOV
1275
        for discipline in cur_dg.discipline_set.all():
×
UNCOV
1276
            if discipline.name == "weterynaria":
×
UNCOV
1277
                pass
×
1278
            # Każda dyscyplina z aktualnego słownika powinna być wpisana do systemu BPP
UNCOV
1279
            try:
×
UNCOV
1280
                TlumaczDyscyplin.objects.get(pbn_2024_now=discipline)
×
UNCOV
1281
            except TlumaczDyscyplin.DoesNotExist:
×
UNCOV
1282
                try:
×
UNCOV
1283
                    dyscyplina_w_bpp = Dyscyplina_Naukowa.objects.get(
×
1284
                        kod=normalize_kod_dyscypliny(discipline.code)
1285
                    )
1286
                    TlumaczDyscyplin.objects.get_or_create(
×
1287
                        dyscyplina_w_bpp=dyscyplina_w_bpp
1288
                    )
1289

UNCOV
1290
                except Dyscyplina_Naukowa.DoesNotExist:
×
UNCOV
1291
                    dyscyplina_w_bpp = Dyscyplina_Naukowa.objects.create(
×
1292
                        kod=normalize_kod_dyscypliny(discipline.code),
1293
                        nazwa=discipline.name,
1294
                    )
UNCOV
1295
                    TlumaczDyscyplin.objects.get_or_create(
×
1296
                        dyscyplina_w_bpp=dyscyplina_w_bpp
1297
                    )
1298

1299
    def interactive(self):
1✔
1300
        self._interactive = True
×
1301
        while True:
×
1302
            cmd = input("cmd> ")
×
1303
            if cmd == "exit":
×
1304
                break
×
1305
            self.exec(cmd.split(" "))
×
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