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

iplweb / bpp / d739ad7a-8bce-4087-b0c8-83f9ea367ad1

18 Feb 2025 12:47AM UTC coverage: 48.187% (+0.7%) from 47.492%
d739ad7a-8bce-4087-b0c8-83f9ea367ad1

push

circleci

mpasternak
Merge branch 'release/v202502.1157'

172 of 381 new or added lines in 31 files covered. (45.14%)

802 existing lines in 49 files now uncovered.

17072 of 35429 relevant lines covered (48.19%)

1.23 hits per line

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

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

9
import requests
3✔
10
from django.db import transaction
3✔
11
from django.db.models import Model
3✔
12
from requests import ConnectionError
3✔
13
from requests.exceptions import SSLError
3✔
14
from simplejson.errors import JSONDecodeError
3✔
15

16
from import_common.core import (
3✔
17
    matchuj_aktualna_dyscypline_pbn,
18
    matchuj_nieaktualna_dyscypline_pbn,
19
)
20
from import_common.normalization import normalize_kod_dyscypliny
3✔
21
from pbn_api.adapters.wydawnictwo import (
3✔
22
    OplataZaWydawnictwoPBNAdapter,
23
    WydawnictwoPBNAdapter,
24
)
25
from pbn_api.const import (
3✔
26
    DEFAULT_BASE_URL,
27
    NEEDS_PBN_AUTH_MSG,
28
    PBN_DELETE_PUBLICATION_STATEMENT,
29
    PBN_GET_DISCIPLINES_URL,
30
    PBN_GET_INSTITUTION_STATEMENTS,
31
    PBN_GET_JOURNAL_BY_ID,
32
    PBN_GET_LANGUAGES_URL,
33
    PBN_GET_PUBLICATION_BY_ID_URL,
34
    PBN_POST_PUBLICATION_FEE_URL,
35
    PBN_POST_PUBLICATIONS_URL,
36
    PBN_SEARCH_PUBLICATIONS_URL,
37
)
38
from pbn_api.exceptions import (
3✔
39
    AccessDeniedException,
40
    AuthenticationConfigurationError,
41
    AuthenticationResponseError,
42
    HttpException,
43
    NeedsPBNAuthorisationException,
44
    NoFeeDataException,
45
    NoPBNUIDException,
46
    PraceSerwisoweException,
47
    SameDataUploadedRecently,
48
)
49
from pbn_api.models import TlumaczDyscyplin
3✔
50
from pbn_api.models.discipline import Discipline, DisciplineGroup
3✔
51
from pbn_api.models.sentdata import SentData
3✔
52

53
from django.contrib.contenttypes.models import ContentType
3✔
54

55
from django.utils.itercompat import is_iterable
3✔
56

57

58
def smart_content(content):
3✔
59
    try:
×
60
        return content.decode("utf-8")
×
61
    except UnicodeDecodeError:
×
62
        return content
×
63

64

65
class PBNClientTransport:
3✔
66
    def __init__(self, app_id, app_token, base_url, user_token=None):
3✔
67
        self.app_id = app_id
×
68
        self.app_token = app_token
×
69

70
        self.base_url = base_url
×
71
        if self.base_url is None:
×
72
            self.base_url = DEFAULT_BASE_URL
×
73

74
        self.access_token = user_token
×
75

76

77
class PageableResource:
3✔
78
    def __init__(self, transport, res, url, headers, body=None, method="get"):
3✔
79
        self.url = url
×
80
        self.headers = headers
×
81
        self.transport = transport
×
82
        self.body = body
×
83
        self.method = getattr(transport, method)
×
84

85
        try:
×
86
            self.page_0 = res["content"]
×
87
        except KeyError:
×
88
            self.page_0 = []
×
89

90
        self.current_page = res["number"]
×
91
        self.total_elements = res["totalElements"]
×
92
        self.total_pages = res["totalPages"]
×
93
        self.done = False
×
94

95
    def count(self):
3✔
96
        return self.total_elements
×
97

98
    def fetch_page(self, current_page):
3✔
99
        if current_page == 0:
×
100
            return self.page_0
×
101
        # print(f"FETCH {current_page}")
102

103
        kw = {"headers": self.headers}
×
104
        if self.body:
×
105
            kw["body"] = self.body
×
106

107
        ret = self.method(self.url + f"&page={current_page}", **kw)
×
108
        # print(f"FETCH DONE {current_page}")
109

110
        try:
×
111
            return ret["content"]
×
112
        except KeyError:
×
113
            return
×
114

115
    def __iter__(self):
3✔
116
        for n in range(0, self.total_pages):
×
117
            yield from self.fetch_page(n)
×
118

119

120
class OAuthMixin:
3✔
121
    @classmethod
3✔
122
    def get_auth_url(klass, base_url, app_id):
3✔
123
        return f"{base_url}/auth/pbn/api/registration/user/token/{app_id}"
×
124

125
    @classmethod
3✔
126
    def get_user_token(klass, base_url, app_id, app_token, one_time_token):
3✔
127
        headers = {
×
128
            "X-App-Id": app_id,
129
            "X-App-Token": app_token,
130
        }
131
        body = {"oneTimeToken": one_time_token}
×
132
        url = f"{base_url}/auth/pbn/api/user/token"
×
133
        response = requests.post(url=url, json=body, headers=headers)
×
134
        try:
×
135
            response.json()
×
136
        except ValueError:
×
137
            if response.content.startswith(b"Mismatched X-APP-TOKEN: "):
×
138
                raise AuthenticationConfigurationError(
×
139
                    "Token aplikacji PBN nieprawidłowy. Poproś administratora "
140
                    "o skonfigurowanie prawidłowego tokena aplikacji PBN w "
141
                    "ustawieniach obiektu Uczelnia. "
142
                )
143

144
            raise AuthenticationResponseError(response.content)
×
145

146
        return response.json().get("X-User-Token")
×
147

148
    def authorize(self, base_url, app_id, app_token):
3✔
149
        from pbn_api.conf import settings
×
150

151
        if self.access_token:
×
152
            return True
×
153

154
        self.access_token = getattr(settings, "PBN_CLIENT_USER_TOKEN")
×
155
        if self.access_token:
×
156
            return True
×
157

158
        auth_url = OAuthMixin.get_auth_url(base_url, app_id)
×
159

160
        print(
×
161
            f"""I have launched a web browser with {auth_url} ,\nplease log-in,
162
             then paste the redirected URL below. \n"""
163
        )
164
        import webbrowser
×
165

166
        webbrowser.open(auth_url)
×
167
        redirect_response = input("Paste the full redirect URL here:")
×
168
        one_time_token = parse_qs(urlparse(redirect_response).query).get("ott")[0]
×
169
        print("ONE TIME TOKEN", one_time_token)
×
170

171
        self.access_token = OAuthMixin.get_user_token(
×
172
            base_url, app_id, app_token, one_time_token
173
        )
174

175
        print("ACCESS TOKEN", self.access_token)
×
176
        return True
×
177

178

179
class RequestsTransport(OAuthMixin, PBNClientTransport):
3✔
180
    def get(self, url, headers=None, fail_on_auth_missing=False):
3✔
181
        sent_headers = {"X-App-Id": self.app_id, "X-App-Token": self.app_token}
×
182
        if self.access_token:
×
183
            sent_headers["X-User-Token"] = self.access_token
×
184

185
        # Jeżeli ustawimy taki nagłówek dla "niewinnych" zapytań GET, to PBN
186
        # API odrzuca takie połączenie z kodem 403, stąd nie:
187
        # if hasattr(self, "access_token"):
188
        #     sent_headers["X-User-Token"] = self.access_token
189

190
        if headers is not None:
×
191
            sent_headers.update(headers)
×
192

193
        retries = 0
×
194
        MAX_RETRIES = 15
×
195

196
        while retries < MAX_RETRIES:
×
197
            try:
×
198
                ret = requests.get(self.base_url + url, headers=sent_headers)
×
199
                break
×
200
            except (SSLError, ConnectionError) as e:
×
201
                retries += 1
×
202
                time.sleep(random.randint(1, 5))
×
203
                if retries >= MAX_RETRIES:
×
204
                    raise e
×
205

206
        if ret.status_code == 403:
×
207
            if fail_on_auth_missing:
×
208
                raise AccessDeniedException(url, smart_content(ret.content))
×
209
            # Needs auth
210
            if ret.json()["message"] == "Access Denied":
×
211
                # Autoryzacja użytkownika jest poprawna, jednakże nie ma on po stronie PBN
212
                # takiego uprawnienia...
213
                raise AccessDeniedException(url, smart_content(ret.content))
×
214

215
            # elif ret.json['message'] == "Forbidden":  # <== to dostaniemy, gdy token zły lub brak
216

217
            if hasattr(self, "authorize"):
×
218
                ret = self.authorize(self.base_url, self.app_id, self.app_token)
×
219
                if not ret:
×
220
                    return
×
221

222
                # Podejmuj ponowną próbę tylko w przypadku udanej autoryzacji
223
                return self.get(url, headers, fail_on_auth_missing=True)
×
224

225
        if ret.status_code >= 400:
×
226
            raise HttpException(ret.status_code, url, smart_content(ret.content))
×
227

228
        try:
×
229
            return ret.json()
×
230
        except JSONDecodeError as e:
×
231
            if ret.status_code == 200 and b"prace serwisowe" in ret.content:
×
232
                # open("pbn_client_dump.html", "wb").write(ret.content)
233
                raise PraceSerwisoweException()
×
234
            raise e
×
235

236
    def post(self, url, headers=None, body=None, delete=False):
3✔
237
        if not hasattr(self, "access_token"):
×
238
            ret = self.authorize(self.base_url, self.app_id, self.app_token)
×
239
            if not ret:
×
240
                return
×
241
            return self.post(url, headers=headers, body=body, delete=delete)
×
242

243
        sent_headers = {
×
244
            "X-App-Id": self.app_id,
245
            "X-App-Token": self.app_token,
246
            "X-User-Token": self.access_token,
247
        }
248

249
        if headers is not None:
×
250
            sent_headers.update(headers)
×
251

252
        method = requests.post
×
253
        if delete:
×
254
            method = requests.delete
×
255

256
        ret = method(self.base_url + url, headers=sent_headers, json=body)
×
257
        if ret.status_code == 403:
×
258
            try:
×
259
                ret_json = ret.json()
×
260
            except BaseException:
×
261
                raise HttpException(
×
262
                    ret.status_code,
263
                    url,
264
                    "Blad podczas odkodowywania JSON podczas odpowiedzi 403: "
265
                    + smart_content(ret.content),
266
                )
267

268
            # Needs auth
269
            if ret_json.get("message") == "Access Denied":
×
270
                # Autoryzacja użytkownika jest poprawna, jednakże nie ma on po stronie PBN
271
                # takiego uprawnienia...
272
                raise AccessDeniedException(url, smart_content(ret.content))
×
273

274
            if ret_json.get("message") == "Forbidden" and ret_json.get(
×
275
                "description"
276
            ).startswith(NEEDS_PBN_AUTH_MSG):
277
                # (403, '/api/v1/search/publications?size=10', '{"code":403,"message":"Forbidden",
278
                # "description":"W celu poprawnej autentykacji należy podać poprawny token użytkownika aplikacji. Podany
279
                # token użytkownika ... w ramach aplikacji ... nie istnieje lub został
280
                # unieważniony!"}')
281
                raise NeedsPBNAuthorisationException(
×
282
                    ret.status_code, url, smart_content(ret.content)
283
                )
284

285
            # mpasternak, 5.09.2021: nie do końca jestem pewny, czy kod wywołujący self.authorize (nast. 2 linijki)
286
            # zostawić w tej sytuacji. Tak było 'historycznie', ale widzę też, że po wywołaniu self.authorize
287
            # ta funkcja zawsze wykonywała "if ret.status_code >= 403" i zwracała Exception. Teoretycznie
288
            # to chyba zostało tutaj z powodu klienta command-line, który to na ten moment priorytetem
289
            # przecież nie jest.
290
            if hasattr(self, "authorize"):
×
291
                self.authorize(self.base_url, self.app_id, self.app_token)
×
292
                # self.authorize()
293

294
        #
295
        # mpasternak 7.09.2021, poniżej "przymiarki" do analizowania zwróconych błędów z PBN
296
        #
297
        # if ret.status_code == 400:
298
        #     try:
299
        #         ret_json = ret.json()
300
        #     except BaseException:
301
        #         raise HttpException(
302
        #             ret.status_code,
303
        #             url,
304
        #             "Blad podczas odkodowywania JSON podczas odpowiedzi 400: "
305
        #             + smart_content(ret.content),
306
        #         )
307
        #     if ret_json.get("message") == "Bad Request" and ret_json.get("description") == "Validation failed."
308
        #     and ret_json.get("details")
309
        #
310
        #     HttpException(400, '/api/v1/publications',
311
        #                   '{"code":400,"message":"Bad Request","description":"Validation failed.",
312
        #                   "details":{"doi":"DOI jest błędny lub nie udało się pobrać informacji z serwisu DOI!"}}')
313

314
        if ret.status_code >= 400:
×
315
            raise HttpException(ret.status_code, url, smart_content(ret.content))
×
316

317
        try:
×
318
            return ret.json()
×
319
        except JSONDecodeError as e:
×
320
            if ret.status_code == 200:
×
321
                if ret.content == b"":
×
322
                    return
×
323

324
                if b"prace serwisowe" in ret.content:
×
325
                    # open("pbn_client_dump.html", "wb").write(ret.content)
326
                    raise PraceSerwisoweException()
×
327

328
            raise e
×
329

330
    def delete(
3✔
331
        self,
332
        url,
333
        headers=None,
334
        body=None,
335
    ):
336
        return self.post(url, headers, body, delete=True)
×
337

338
    def _pages(self, method, url, headers=None, body=None, page_size=10, *args, **kw):
3✔
339
        # Stronicowanie zwraca rezultaty w taki sposób:
340
        # {'content': [{'mongoId': '5e709189878c28a04737dc6f',
341
        #               'status': 'ACTIVE',
342
        # ...
343
        #              'versionHash': '---'}]}],
344
        #  'first': True,
345
        #  'last': False,
346
        #  'number': 0,
347
        #  'numberOfElements': 10,
348
        #  'pageable': {'offset': 0,
349
        #               'pageNumber': 0,
350
        #               'pageSize': 10,
351
        #               'paged': True,
352
        #               'sort': {'sorted': False, 'unsorted': True},
353
        #               'unpaged': False},
354
        #  'size': 10,
355
        #  'sort': {'sorted': False, 'unsorted': True},
356
        #  'totalElements': 68577,
357
        #  'totalPages': 6858}
358

359
        chr = "?"
×
360
        if url.find("?") >= 0:
×
361
            chr = "&"
×
362

363
        url = url + f"{chr}size={page_size}"
×
364
        chr = "&"
×
365

366
        for elem in kw:
×
367
            url += chr + elem + "=" + quote(kw[elem])
×
368

369
        method_function = getattr(self, method)
×
370

371
        if method == "get":
×
372
            res = method_function(url, headers)
×
373
        elif method == "post":
×
374
            res = method_function(url, headers, body=body)
×
375
        else:
376
            raise NotImplementedError
×
377

378
        if "pageable" not in res:
×
379
            warnings.warn(
×
380
                f"PBNClient.{method}_page request for {url} with headers {headers} did not return a paged resource, "
381
                f"maybe use PBNClient.{method} (without 'page') instead",
382
                RuntimeWarning,
383
            )
384
            return res
×
385
        return PageableResource(
×
386
            self, res, url=url, headers=headers, body=body, method=method
387
        )
388

389
    def get_pages(self, url, headers=None, page_size=10, *args, **kw):
3✔
390
        return self._pages(
×
391
            "get", url=url, headers=headers, page_size=page_size, *args, **kw
392
        )
393

394
    def post_pages(self, url, headers=None, body=None, page_size=10, *args, **kw):
3✔
395
        # Jak get_pages, ale methoda to post
396
        if body is None:
×
397
            body = kw
×
398

399
        return self._pages(
×
400
            "post",
401
            url=url,
402
            headers=headers,
403
            body=body,
404
            page_size=page_size,
405
            *args,
406
            **kw,
407
        )
408

409

410
class ConferencesMixin:
3✔
411
    def get_conferences(self, *args, **kw):
3✔
412
        return self.transport.get_pages("/api/v1/conferences/page", *args, **kw)
×
413

414
    def get_conferences_mnisw(self, *args, **kw):
3✔
415
        return self.transport.get_pages("/api/v1/conferences/mnisw/page", *args, **kw)
×
416

417
    def get_conference(self, id):
3✔
418
        return self.transport.get(f"/api/v1/conferences/{id}")
×
419

420
    def get_conference_editions(self, id):
3✔
421
        return self.transport.get(f"/api/v1/conferences/{id}/editions")
×
422

423
    def get_conference_metadata(self, id):
3✔
424
        return self.transport.get(f"/api/v1/conferences/{id}/metadata")
×
425

426

427
class DictionariesMixin:
3✔
428
    def get_countries(self):
3✔
429
        return self.transport.get("/api/v1/dictionary/countries")
×
430
        return self.transport.get("/api/v1/dictionary/countries")
431

432
    def get_disciplines(self):
3✔
433
        return self.transport.get(PBN_GET_DISCIPLINES_URL)
×
434

435
    def get_languages(self):
3✔
436
        return self.transport.get(PBN_GET_LANGUAGES_URL)
×
437

438

439
class InstitutionsMixin:
3✔
440
    def get_institutions(self, status="ACTIVE", *args, **kw):
3✔
441
        return self.transport.get_pages(
×
442
            "/api/v1/institutions/page", status=status, *args, **kw
443
        )
444

445
    def get_institution_by_id(self, id):
3✔
NEW
446
        return self.transport.get(f"/api/v1/institutions/{id}")
×
447

448
    def get_institution_by_version(self, version):
3✔
449
        return self.transport.get_pages(f"/api/v1/institutions/version/{version}")
×
450

451
    def get_institution_metadata(self, id):
3✔
452
        return self.transport.get_pages(f"/api/v1/institutions/{id}/metadata")
×
453

454
    def get_institutions_polon(self, includeAllVersions="true", *args, **kw):
3✔
455
        return self.transport.get_pages(
×
456
            "/api/v1/institutions/polon/page",
457
            includeAllVersions=includeAllVersions,
458
            *args,
459
            **kw,
460
        )
461

462
    def get_institutions_polon_by_uid(self, uid):
3✔
463
        return self.transport.get(f"/api/v1/institutions/polon/uid/{uid}")
×
464

465
    def get_institutions_polon_by_id(self, id):
3✔
466
        return self.transport.get(f"/api/v1/institutions/polon/{id}")
×
467

468

469
class InstitutionsProfileMixin:
3✔
470
    # XXX: wymaga autoryzacji
471
    def get_institution_publications(self, page_size=10):
3✔
472
        return self.transport.get_pages(
×
473
            "/api/v1/institutionProfile/publications/page", page_size=page_size
474
        )
475

476
    def get_institution_statements(self, page_size=10):
3✔
477
        return self.transport.get_pages(
×
478
            PBN_GET_INSTITUTION_STATEMENTS,
479
            page_size=page_size,
480
        )
481

482
    def get_institution_statements_of_single_publication(
3✔
483
        self, pbn_uid_id, page_size=50
484
    ):
485
        return self.transport.get_pages(
×
486
            PBN_GET_INSTITUTION_STATEMENTS + "?publicationId=" + pbn_uid_id,
487
            page_size=page_size,
488
        )
489

490
    def delete_all_publication_statements(self, publicationId):
3✔
491
        return self.transport.delete(
×
492
            PBN_DELETE_PUBLICATION_STATEMENT.format(publicationId=publicationId),
493
            body={"all": True, "statementsOfPersons": []},
494
        )
495

496
    def delete_publication_statement(self, publicationId, personId, role):
3✔
497
        return self.transport.delete(
×
498
            PBN_DELETE_PUBLICATION_STATEMENT.format(publicationId=publicationId),
499
            body={"statementsOfPersons": [{"personId": personId, "role": role}]},
500
        )
501

502

503
class JournalsMixin:
3✔
504
    def get_journals_mnisw(self, *args, **kw):
3✔
505
        return self.transport.get_pages("/api/v1/journals/mnisw/page", *args, **kw)
×
506

507
    def get_journals_mnisw_v2(self, *args, **kw):
3✔
508
        return self.transport.get_pages("/api/v2/journals/mnisw/page", *args, **kw)
×
509

510
    def get_journals(self, *args, **kw):
3✔
511
        return self.transport.get_pages("/api/v1/journals/page", *args, **kw)
×
512

513
    def get_journals_v2(self, *args, **kw):
3✔
514
        return self.transport.get_pages("/api/v2/journals/page", *args, **kw)
×
515

516
    def get_journal_by_version(self, version):
3✔
517
        return self.transport.get(f"/api/v1/journals/version/{version}")
×
518

519
    def get_journal_by_id(self, id):
3✔
520
        return self.transport.get(PBN_GET_JOURNAL_BY_ID.format(id=id))
×
521

522
    def get_journal_metadata(self, id):
3✔
523
        return self.transport.get(f"/api/v1/journals/{id}/metadata")
×
524

525

526
class PersonMixin:
3✔
527
    def get_people_by_institution_id(self, id):
3✔
528
        return self.transport.get(f"/api/v1/person/institution/{id}")
×
529

530
    def get_person_by_natural_id(self, id):
3✔
531
        return self.transport.get(f"/api/v1/person/natural/{id}")
×
532

533
    def get_person_by_orcid(self, orcid):
3✔
534
        return self.transport.get(f"/api/v1/person/orcid/{orcid}")
×
535

536
    def get_people(self, *args, **kw):
3✔
537
        return self.transport.get_pages("/api/v1/person/page", *args, **kw)
×
538

539
    def get_person_by_polon_uid(self, uid):
3✔
540
        return self.transport.get(f"/api/v1/person/polon/{uid}")
×
541

542
    def get_person_by_version(self, version):
3✔
543
        return self.transport.get(f"/api/v1/person/version/{version}")
×
544

545
    def get_person_by_id(self, id):
3✔
546
        return self.transport.get(f"/api/v1/person/{id}")
×
547

548

549
class PublishersMixin:
3✔
550
    def get_publishers_mnisw(self, *args, **kw):
3✔
551
        return self.transport.get_pages("/api/v1/publishers/mnisw/page", *args, **kw)
×
552

553
    def get_publishers_mnisw_yearlist(self, *args, **kw):
3✔
554
        return self.transport.get_pages(
×
555
            "/api/v1/publishers/mnisw/page/yearlist", *args, **kw
556
        )
557

558
    def get_publishers(self, *args, **kw):
3✔
559
        return self.transport.get_pages("/api/v1/publishers/page", *args, **kw)
×
560

561
    def get_publisher_by_version(self, version):
3✔
562
        return self.transport.get(f"/api/v1/publishers/version/{version}")
×
563

564
    def get_publisher_by_id(self, id):
3✔
565
        return self.transport.get(f"/api/v1/publishers/{id}")
×
566

567
    def get_publisher_metadata(self, id):
3✔
568
        return self.transport.get(f"/api/v1/publishers/{id}/metadata")
×
569

570

571
class PublicationsMixin:
3✔
572
    def get_publication_by_doi(self, doi):
3✔
573
        return self.transport.get(
×
574
            f"/api/v1/publications/doi/?doi={quote(doi, safe='')}",
575
        )
576

577
    def get_publication_by_doi_page(self, doi):
3✔
578
        return self.transport.get_pages(
×
579
            f"/api/v1/publications/doi/page?doi={quote(doi, safe='')}",
580
            headers={"doi": doi},
581
        )
582

583
    def get_publication_by_id(self, id):
3✔
584
        return self.transport.get(PBN_GET_PUBLICATION_BY_ID_URL.format(id=id))
×
585

586
    def get_publication_metadata(self, id):
3✔
587
        return self.transport.get(f"/api/v1/publications/id/{id}/metadata")
×
588

589
    def get_publications(self, **kw):
3✔
590
        return self.transport.get_pages("/api/v1/publications/page", **kw)
×
591

592
    def get_publication_by_version(self, version):
3✔
593
        return self.transport.get(f"/api/v1/publications/version/{version}")
×
594

595

596
class SearchMixin:
3✔
597
    def search_publications(self, *args, **kw):
3✔
598
        return self.transport.post_pages(PBN_SEARCH_PUBLICATIONS_URL, body=kw)
×
599

600

601
class PBNClient(
3✔
602
    ConferencesMixin,
603
    DictionariesMixin,
604
    InstitutionsMixin,
605
    InstitutionsProfileMixin,
606
    JournalsMixin,
607
    PersonMixin,
608
    PublicationsMixin,
609
    PublishersMixin,
610
    SearchMixin,
611
):
612
    _interactive = False
3✔
613

614
    def __init__(self, transport: RequestsTransport):
3✔
615
        self.transport = transport
1✔
616

617
    def post_publication(self, json):
3✔
618
        return self.transport.post(PBN_POST_PUBLICATIONS_URL, body=json)
×
619

620
    def post_publication_fee(self, publicationId, json):
3✔
621
        return self.transport.post(
×
622
            PBN_POST_PUBLICATION_FEE_URL.format(id=publicationId), body=json
623
        )
624

625
    def get_publication_fee(self, publicationId):
3✔
626
        res = self.transport.post_pages(
×
627
            "/api/v1/institutionProfile/publications/search/fees",
628
            body={"publicationIds": [str(publicationId)]},
629
        )
630
        if not res.count():
×
631
            return
×
632
        elif res.count() == 1:
×
633
            return list(res)[0]
×
634
        else:
635
            raise NotImplementedError("count > 1")
×
636

637
    def upload_publication(
3✔
638
        self, rec, force_upload=False, export_pk_zero=None, always_affiliate_to_uid=None
639
    ):
640
        js = WydawnictwoPBNAdapter(
×
641
            rec,
642
            export_pk_zero=export_pk_zero,
643
            always_affiliate_to_uid=always_affiliate_to_uid,
644
        ).pbn_get_json()
645
        if not force_upload:
×
646
            needed = SentData.objects.check_if_needed(rec, js)
×
647
            if not needed:
×
648
                raise SameDataUploadedRecently(
×
649
                    SentData.objects.get_for_rec(rec).last_updated_on
650
                )
651
        try:
×
652
            ret = self.post_publication(js)
×
653
        except Exception as e:
×
654
            SentData.objects.updated(rec, js, uploaded_okay=False, exception=str(e))
×
655
            raise e
×
656

657
        return ret, js
×
658

659
    def download_publication(self, doi=None, objectId=None):
3✔
660
        from .integrator import zapisz_mongodb
×
661
        from .models import Publication
×
662

663
        assert doi or objectId
×
664

665
        if doi:
×
666
            data = self.get_publication_by_doi(doi)
×
667
        elif objectId:
×
668
            data = self.get_publication_by_id(objectId)
×
669

670
        return zapisz_mongodb(data, Publication)
×
671

672
    @transaction.atomic
3✔
673
    def download_statements_of_publication(self, pub):
3✔
674
        from pbn_api.models import OswiadczenieInstytucji
×
675
        from .integrator import pobierz_mongodb, zapisz_oswiadczenie_instytucji
×
676

677
        OswiadczenieInstytucji.objects.filter(publicationId_id=pub.pk).delete()
×
678

679
        pobierz_mongodb(
×
680
            self.get_institution_statements_of_single_publication(pub.pk, 5120),
681
            None,
682
            fun=zapisz_oswiadczenie_instytucji,
683
            client=self,
684
            disable_progress_bar=True,
685
        )
686

687
    def sync_publication(
3✔
688
        self,
689
        pub,
690
        force_upload=False,
691
        delete_statements_before_upload=False,
692
        export_pk_zero=None,
693
        always_affiliate_to_uid=None,
694
    ):
695
        """
696
        @param delete_statements_before_upload: gdy True, kasuj oświadczenia publikacji przed wysłaniem (jeżeli posiada
697
        PBN UID)
698
        """
699

700
        # if not pub.doi:
701
        #     raise WillNotExportError("Ustaw DOI dla publikacji")
702

703
        pub = self.eventually_coerce_to_publication(pub)
×
704

705
        #
706
        if (
×
707
            delete_statements_before_upload
708
            and hasattr(pub, "pbn_uid_id")
709
            and pub.pbn_uid_id is not None
710
        ):
711
            try:
×
712
                self.delete_all_publication_statements(pub.pbn_uid_id)
×
713

714
                # Jeżeli zostały skasowane dane, to wymuś wysłanie rekordu, niezależnie
715
                # od stanu tabeli SentData
716
                force_upload = True
×
717
            except HttpException as e:
×
718
                NIE_ISTNIEJA = "Nie istnieją oświadczenia dla publikacji"
×
719

720
                ignored_exception = False
×
721

722
                if e.status_code == 400:
×
723
                    if e.json:
×
724
                        try:
×
725
                            try:
×
726
                                msg = e.json["details"]["publicationId"]
×
727
                            except KeyError:
×
728
                                msg = e.json["details"][
×
729
                                    f"publicationId.{pub.pbn_uid_id}"
730
                                ]
731
                            if NIE_ISTNIEJA in msg:
×
732
                                ignored_exception = True
×
733
                        except (TypeError, KeyError):
×
734
                            if NIE_ISTNIEJA in e.content:
×
735
                                ignored_exception = True
×
736

737
                    else:
738
                        if NIE_ISTNIEJA in e.content:
×
739
                            ignored_exception = True
×
740

741
                if not ignored_exception:
×
742
                    raise e
×
743

744
        # Wgraj dane do PBN
745
        ret, js = self.upload_publication(
×
746
            pub,
747
            force_upload=force_upload,
748
            export_pk_zero=export_pk_zero,
749
            always_affiliate_to_uid=always_affiliate_to_uid,
750
        )
751

752
        # Pobierz zwrotnie dane z PBN
753
        publication = self.download_publication(objectId=ret["objectId"])
×
754
        self.download_statements_of_publication(publication)
×
755

756
        # Utwórz obiekt zapisanych danych. Dopiero w tym miejscu, bo jeżeli zostanie
757
        # utworzony nowy rekord po stronie PBN, to pbn_uid_id musi wskazywać na
758
        # bazę w tabeli Publication, która została chwile temu pobrana...
759
        SentData.objects.updated(pub, js, pbn_uid_id=ret["objectId"])
×
760

761
        if pub.pbn_uid_id != ret["objectId"]:
×
762
            pub.pbn_uid = publication
×
763
            pub.save()
×
764

765
    def eventually_coerce_to_publication(self, pub: Model | str) -> Model:
3✔
766
        if type(pub) is str:
×
767
            # Ciag znaków w postaci wydawnictwo_zwarte:123 pozwoli na podawanie tego
768
            # parametru do wywołań z linii poleceń
769
            model, pk = pub.split(":")
×
770
            ctype = ContentType.objects.get(app_label="bpp", model=model)
×
771
            pub = ctype.model_class().objects.get(pk=pk)
×
772

773
        return pub
×
774

775
    def upload_publication_fee(self, pub: Model):
3✔
776
        pub = self.eventually_coerce_to_publication(pub)
×
777
        if pub.pbn_uid_id is None:
×
778
            raise NoPBNUIDException(
×
779
                f"PBN UID (czyli 'numer odpowiednika w PBN') dla rekordu '{pub}' jest pusty."
780
            )
781

782
        fee = OplataZaWydawnictwoPBNAdapter(pub).pbn_get_json()
×
783
        if not fee:
×
784
            raise NoFeeDataException(
×
785
                f"Brak danych o opłatach za publikację {pub.pbn_uid_id}"
786
            )
787

788
        return self.post_publication_fee(pub.pbn_uid_id, fee)
×
789

790
    def exec(self, cmd):
3✔
791
        try:
×
792
            fun = getattr(self, cmd[0])
×
793
        except AttributeError as e:
×
794
            if self._interactive:
×
795
                print("No such command: %s" % cmd)
×
796
                return
×
797
            else:
798
                raise e
×
799

800
        def extract_arguments(lst):
×
801
            args = ()
×
802
            kw = {}
×
803
            for elem in lst:
×
804
                if elem.find(":") >= 1:
×
805
                    k, n = elem.split(":", 1)
×
806
                    kw[k] = n
×
807
                else:
808
                    args += (elem,)
×
809

810
            return args, kw
×
811

812
        args, kw = extract_arguments(cmd[1:])
×
813
        res = fun(*args, **kw)
×
814

815
        if not sys.stdout.isatty():
×
816
            # Non-interactive mode, just output the json
817
            import json
×
818

819
            print(json.dumps(res))
×
820
        else:
821
            if type(res) is dict:
×
822
                pprint(res)
×
823
            elif is_iterable(res):
×
824
                if self._interactive and hasattr(res, "total_elements"):
×
825
                    print(
×
826
                        "Incoming data: no_elements=",
827
                        res.total_elements,
828
                        "no_pages=",
829
                        res.total_pages,
830
                    )
831
                    input("Press ENTER to continue> ")
×
832
                for elem in res:
×
833
                    pprint(elem)
×
834

835
    @transaction.atomic
3✔
836
    def download_disciplines(self):
3✔
837
        """Zapisuje słownik dyscyplin z API PBN do lokalnej bazy"""
838

839
        for elem in self.get_disciplines():
×
840
            validityDateFrom = elem.get("validityDateFrom", None)
×
841
            validityDateTo = elem.get("validityDateTo", None)
×
842
            uuid = elem["uuid"]
×
843

844
            parent_group, created = DisciplineGroup.objects.update_or_create(
×
845
                uuid=uuid,
846
                defaults={
847
                    "validityDateFrom": validityDateFrom,
848
                    "validityDateTo": validityDateTo,
849
                },
850
            )
851

852
            for discipline in elem["disciplines"]:
×
853
                # print("XXX", discipline["uuid"])
854
                Discipline.objects.update_or_create(
×
855
                    parent_group=parent_group,
856
                    uuid=discipline["uuid"],
857
                    defaults=dict(
858
                        code=discipline["code"],
859
                        name=discipline["name"],
860
                        polonCode=discipline["polonCode"],
861
                        scientificFieldName=discipline["scientificFieldName"],
862
                    ),
863
                )
864

865
    @transaction.atomic
3✔
866
    def sync_disciplines(self):
3✔
867
        self.download_disciplines()
×
868
        try:
×
869
            cur_dg = DisciplineGroup.objects.get_current()
×
870
        except DisciplineGroup.DoesNotExist:
×
871
            raise ValueError(
×
872
                "Brak aktualnego słownika dyscyplin na serwerze. Pobierz aktualny słownik "
873
                "dyscyplin z PBN."
874
            )
875

876
        from bpp.models import Dyscyplina_Naukowa
×
877

878
        for dyscyplina in Dyscyplina_Naukowa.objects.all():
×
879
            wpis_tlumacza = TlumaczDyscyplin.objects.get_or_create(
×
880
                dyscyplina_w_bpp=dyscyplina
881
            )[0]
882

883
            wpis_tlumacza.pbn_2022_now = matchuj_aktualna_dyscypline_pbn(
×
884
                dyscyplina.kod, dyscyplina.nazwa
885
            )
886
            # Domyślnie szuka dla lat 2018-2022
887
            wpis_tlumacza.pbn_2017_2021 = matchuj_nieaktualna_dyscypline_pbn(
×
888
                dyscyplina.kod, dyscyplina.nazwa
889
            )
890

891
            wpis_tlumacza.save()
×
892

893
        for discipline in cur_dg.discipline_set.all():
×
894
            if discipline.name == "weterynaria":
×
895
                pass
×
896
            # Każda dyscyplina z aktualnego słownika powinna być wpisana do systemu BPP
897
            try:
×
898
                TlumaczDyscyplin.objects.get(pbn_2022_now=discipline)
×
899
            except TlumaczDyscyplin.DoesNotExist:
×
900
                try:
×
901
                    dyscyplina_w_bpp = Dyscyplina_Naukowa.objects.get(
×
902
                        kod=normalize_kod_dyscypliny(discipline.code)
903
                    )
904
                    TlumaczDyscyplin.objects.get_or_create(
×
905
                        dyscyplina_w_bpp=dyscyplina_w_bpp
906
                    )
907

908
                except Dyscyplina_Naukowa.DoesNotExist:
×
909
                    dyscyplina_w_bpp = Dyscyplina_Naukowa.objects.create(
×
910
                        kod=normalize_kod_dyscypliny(discipline.code),
911
                        nazwa=discipline.name,
912
                    )
913
                    TlumaczDyscyplin.objects.get_or_create(
×
914
                        dyscyplina_w_bpp=dyscyplina_w_bpp
915
                    )
916

917
    def interactive(self):
3✔
918
        self._interactive = True
×
919
        while True:
×
920
            cmd = input("cmd> ")
×
921
            if cmd == "exit":
×
922
                break
×
923
            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