Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
course-discovery
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
course-discovery
Commits
f08645d2
Commit
f08645d2
authored
Jan 23, 2017
by
Simon Chen
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update the marketing api loader to login properly
ECOM-6880
parent
c0b56065
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
195 additions
and
178 deletions
+195
-178
course_discovery/apps/course_metadata/admin.py
+2
-1
course_discovery/apps/course_metadata/data_loaders/marketing_site.py
+7
-17
course_discovery/apps/course_metadata/data_loaders/tests/test_marketing_site.py
+14
-0
course_discovery/apps/course_metadata/exceptions.py
+12
-0
course_discovery/apps/course_metadata/publishers.py
+2
-75
course_discovery/apps/course_metadata/tests/test_publishers.py
+2
-85
course_discovery/apps/course_metadata/tests/test_utils.py
+84
-0
course_discovery/apps/course_metadata/utils.py
+72
-0
No files found.
course_discovery/apps/course_metadata/admin.py
View file @
f08645d2
...
@@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy as _
...
@@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy as _
from
course_discovery.apps.course_metadata.forms
import
ProgramAdminForm
,
CourseAdminForm
from
course_discovery.apps.course_metadata.forms
import
ProgramAdminForm
,
CourseAdminForm
from
course_discovery.apps.course_metadata.models
import
*
# pylint: disable=wildcard-import
from
course_discovery.apps.course_metadata.models
import
*
# pylint: disable=wildcard-import
from
course_discovery.apps.course_metadata.publishers
import
ProgramPublisherException
from
course_discovery.apps.course_metadata.publishers
import
ProgramPublisherException
from
course_discovery.apps.course_metadata.utils
import
MarketingSiteAPIClientException
class
SeatInline
(
admin
.
TabularInline
):
class
SeatInline
(
admin
.
TabularInline
):
...
@@ -123,7 +124,7 @@ class ProgramAdmin(admin.ModelAdmin):
...
@@ -123,7 +124,7 @@ class ProgramAdmin(admin.ModelAdmin):
try
:
try
:
super
()
.
save_model
(
request
,
obj
,
form
,
change
)
super
()
.
save_model
(
request
,
obj
,
form
,
change
)
self
.
save_error
=
False
self
.
save_error
=
False
except
ProgramPublisherException
:
except
(
ProgramPublisherException
,
MarketingSiteAPIClientException
)
:
# TODO Redirect the user back to the form so that he/she can try again.
# TODO Redirect the user back to the form so that he/she can try again.
logger
.
exception
(
'An error occurred while publishing the program [
%
s] to the marketing site.'
,
obj
.
uuid
)
logger
.
exception
(
'An error occurred while publishing the program [
%
s] to the marketing site.'
,
obj
.
uuid
)
msg
=
_
(
'An error occurred while publishing the program to the marketing site. Please try again. '
msg
=
_
(
'An error occurred while publishing the program to the marketing site. Please try again. '
...
...
course_discovery/apps/course_metadata/data_loaders/marketing_site.py
View file @
f08645d2
...
@@ -7,7 +7,6 @@ from uuid import UUID
...
@@ -7,7 +7,6 @@ from uuid import UUID
from
dateutil
import
rrule
from
dateutil
import
rrule
import
pytz
import
pytz
import
requests
from
django.db.models
import
Q
from
django.db.models
import
Q
from
django.utils.functional
import
cached_property
from
django.utils.functional
import
cached_property
from
opaque_keys
import
InvalidKeyError
from
opaque_keys
import
InvalidKeyError
...
@@ -18,6 +17,7 @@ from course_discovery.apps.course_metadata.data_loaders import AbstractDataLoade
...
@@ -18,6 +17,7 @@ from course_discovery.apps.course_metadata.data_loaders import AbstractDataLoade
from
course_discovery.apps.course_metadata.models
import
(
from
course_discovery.apps.course_metadata.models
import
(
Course
,
Organization
,
Person
,
Subject
,
Program
,
Position
,
LevelType
,
CourseRun
Course
,
Organization
,
Person
,
Subject
,
Program
,
Position
,
LevelType
,
CourseRun
)
)
from
course_discovery.apps.course_metadata.utils
import
MarketingSiteAPIClient
from
course_discovery.apps.ietf_language_tags.models
import
LanguageTag
from
course_discovery.apps.ietf_language_tags.models
import
LanguageTag
logger
=
logging
.
getLogger
(
__name__
)
logger
=
logging
.
getLogger
(
__name__
)
...
@@ -37,24 +37,14 @@ class AbstractMarketingSiteDataLoader(AbstractDataLoader):
...
@@ -37,24 +37,14 @@ class AbstractMarketingSiteDataLoader(AbstractDataLoader):
@cached_property
@cached_property
def
api_client
(
self
):
def
api_client
(
self
):
username
=
self
.
partner
.
marketing_site_api_username
# Login by posting to the login form
login_data
=
{
'name'
:
username
,
'pass'
:
self
.
partner
.
marketing_site_api_password
,
'form_id'
:
'user_login'
,
'op'
:
'Log in'
,
}
session
=
requests
.
Session
()
marketing_site_api_client
=
MarketingSiteAPIClient
(
login_url
=
'{root}/user'
.
format
(
root
=
self
.
api_url
)
self
.
partner
.
marketing_site_api_username
,
response
=
session
.
post
(
login_url
,
data
=
login_data
)
self
.
partner
.
marketing_site_api_password
,
expected_url
=
'{root}/users/{username}'
.
format
(
root
=
self
.
api_url
,
username
=
username
)
self
.
api_url
if
not
(
response
.
status_code
==
200
and
response
.
url
==
expected_url
):
)
raise
Exception
(
'Login failed!'
)
return
session
return
marketing_site_api_client
.
api_
session
def
get_query_kwargs
(
self
):
def
get_query_kwargs
(
self
):
return
{
return
{
...
...
course_discovery/apps/course_metadata/data_loaders/tests/test_marketing_site.py
View file @
f08645d2
...
@@ -87,6 +87,20 @@ class AbstractMarketingSiteDataLoaderTestMixin(DataLoaderTestMixin):
...
@@ -87,6 +87,20 @@ class AbstractMarketingSiteDataLoaderTestMixin(DataLoaderTestMixin):
responses
.
add
(
responses
.
POST
,
url
,
status
=
status
,
adding_headers
=
adding_headers
)
responses
.
add
(
responses
.
POST
,
url
,
status
=
status
,
adding_headers
=
adding_headers
)
responses
.
add
(
responses
.
GET
,
landing_url
)
responses
.
add
(
responses
.
GET
,
landing_url
)
responses
.
add
(
responses
.
GET
,
'{root}admin'
.
format
(
root
=
self
.
api_url
),
status
=
(
500
if
failure
else
200
)
)
responses
.
add
(
responses
.
GET
,
'{root}restws/session/token'
.
format
(
root
=
self
.
api_url
),
body
=
'test token'
,
content_type
=
'text/html'
,
status
=
200
)
def
mock_api_failure
(
self
):
def
mock_api_failure
(
self
):
url
=
self
.
api_url
+
'node.json'
url
=
self
.
api_url
+
'node.json'
responses
.
add
(
responses
.
GET
,
url
,
status
=
500
)
responses
.
add
(
responses
.
GET
,
url
,
status
=
500
)
...
...
course_discovery/apps/course_metadata/exceptions.py
0 → 100644
View file @
f08645d2
class
MarketingSiteAPIClientException
(
Exception
):
""" The exception thrown from MarketingSiteAPIClient """
pass
class
ProgramPublisherException
(
Exception
):
""" The exception thrown during the program publishing process to marketing site """
def
__init__
(
self
,
message
):
super
(
ProgramPublisherException
,
self
)
.
__init__
(
message
)
suffix
=
'The program data has not been saved. Please check your marketing site configuration'
self
.
message
=
'{exception_msg} {suffix}'
.
format
(
exception_msg
=
message
,
suffix
=
suffix
)
course_discovery/apps/course_metadata/publishers.py
View file @
f08645d2
import
json
import
json
from
bs4
import
BeautifulSoup
from
bs4
import
BeautifulSoup
import
requests
from
django.utils.functional
import
cached_property
from
course_discovery.apps.course_metadata.exceptions
import
ProgramPublisherException
from
course_discovery.apps.course_metadata.utils
import
MarketingSiteAPIClient
class
ProgramPublisherException
(
Exception
):
def
__init__
(
self
,
message
):
super
(
ProgramPublisherException
,
self
)
.
__init__
(
message
)
suffix
=
'The program data has not been saved. Please check your marketing site configuration'
self
.
message
=
'{exception_msg} {suffix}'
.
format
(
exception_msg
=
message
,
suffix
=
suffix
)
class
MarketingSiteAPIClient
(
object
):
"""
The marketing site API client we can use to communicate with the marketing site
"""
username
=
None
password
=
None
api_url
=
None
def
__init__
(
self
,
marketing_site_api_username
,
marketing_site_api_password
,
api_url
):
if
not
(
marketing_site_api_username
and
marketing_site_api_password
):
raise
ProgramPublisherException
(
'Marketing Site API credentials are not properly configured!'
)
self
.
username
=
marketing_site_api_username
self
.
password
=
marketing_site_api_password
self
.
api_url
=
api_url
.
strip
(
'/'
)
@cached_property
def
init_session
(
self
):
# Login to set session cookies
session
=
requests
.
Session
()
login_url
=
'{root}/user'
.
format
(
root
=
self
.
api_url
)
login_data
=
{
'name'
:
self
.
username
,
'pass'
:
self
.
password
,
'form_id'
:
'user_login'
,
'op'
:
'Log in'
,
}
response
=
session
.
post
(
login_url
,
data
=
login_data
)
expected_url
=
'{root}/users/{username}'
.
format
(
root
=
self
.
api_url
,
username
=
self
.
username
)
admin_url
=
'{root}/admin'
.
format
(
root
=
self
.
api_url
)
can_access_admin
=
session
.
get
(
admin_url
)
if
not
(
can_access_admin
.
status_code
==
200
and
response
.
url
==
expected_url
):
raise
ProgramPublisherException
(
'Marketing Site Login failed!'
)
return
session
@cached_property
def
api_session
(
self
):
self
.
init_session
.
headers
.
update
(
self
.
headers
)
return
self
.
init_session
@cached_property
def
csrf_token
(
self
):
token_url
=
'{root}/restws/session/token'
.
format
(
root
=
self
.
api_url
)
response
=
self
.
init_session
.
get
(
token_url
)
if
not
response
.
status_code
==
200
:
raise
ProgramPublisherException
(
'Failed to retrieve Marketing Site CSRF token!'
)
token
=
response
.
content
.
decode
(
'utf8'
)
return
token
@cached_property
def
user_id
(
self
):
# Get a user ID
user_url
=
'{root}/user.json?name={username}'
.
format
(
root
=
self
.
api_url
,
username
=
self
.
username
)
response
=
self
.
init_session
.
get
(
user_url
)
if
not
response
.
status_code
==
200
:
raise
ProgramPublisherException
(
'Failed to retrieve Marketing site user details!'
)
user_id
=
response
.
json
()[
'list'
][
0
][
'uid'
]
return
user_id
@cached_property
def
headers
(
self
):
return
{
'Content-Type'
:
'application/json'
,
'X-CSRF-Token'
:
self
.
csrf_token
,
}
class
MarketingSitePublisher
(
object
):
class
MarketingSitePublisher
(
object
):
...
...
course_discovery/apps/course_metadata/tests/test_publishers.py
View file @
f08645d2
...
@@ -8,92 +8,9 @@ from course_discovery.apps.course_metadata.publishers import (
...
@@ -8,92 +8,9 @@ from course_discovery.apps.course_metadata.publishers import (
ProgramPublisherException
,
ProgramPublisherException
,
)
)
from
course_discovery.apps.course_metadata.tests.factories
import
ProgramFactory
from
course_discovery.apps.course_metadata.tests.factories
import
ProgramFactory
from
course_discovery.apps.course_metadata.tests.mixins
import
(
from
course_discovery.apps.course_metadata.tests.mixins
import
MarketingSitePublisherTestMixin
MarketingSiteAPIClientTestMixin
,
MarketingSitePublisherTestMixin
,
)
from
course_discovery.apps.course_metadata.models
import
ProgramType
class
MarketingSiteAPIClientTests
(
MarketingSiteAPIClientTestMixin
):
"""
Unit test cases for MarketinSiteAPIClient
"""
def
setUp
(
self
):
super
(
MarketingSiteAPIClientTests
,
self
)
.
setUp
()
self
.
api_client
=
MarketingSiteAPIClient
(
self
.
username
,
self
.
password
,
self
.
api_root
)
@responses.activate
def
test_init_session
(
self
):
self
.
mock_login_response
(
200
)
self
.
mock_admin_response
(
200
)
session
=
self
.
api_client
.
init_session
self
.
assert_responses_call_count
(
3
)
self
.
assertIsNotNone
(
session
)
@responses.activate
def
test_init_session_failed
(
self
):
self
.
mock_login_response
(
500
)
self
.
mock_admin_response
(
500
)
with
self
.
assertRaises
(
ProgramPublisherException
):
self
.
api_client
.
init_session
# pylint: disable=pointless-statement
@responses.activate
def
test_csrf_token
(
self
):
self
.
mock_login_response
(
200
)
self
.
mock_admin_response
(
200
)
self
.
mock_csrf_token_response
(
200
)
csrf_token
=
self
.
api_client
.
csrf_token
self
.
assert_responses_call_count
(
4
)
self
.
assertEqual
(
self
.
csrf_token
,
csrf_token
)
@responses.activate
from
course_discovery.apps.course_metadata.models
import
ProgramType
def
test_csrf_token_failed
(
self
):
self
.
mock_login_response
(
200
)
self
.
mock_admin_response
(
200
)
self
.
mock_csrf_token_response
(
500
)
with
self
.
assertRaises
(
ProgramPublisherException
):
self
.
api_client
.
csrf_token
# pylint: disable=pointless-statement
@responses.activate
def
test_user_id
(
self
):
self
.
mock_login_response
(
200
)
self
.
mock_admin_response
(
200
)
self
.
mock_user_id_response
(
200
)
user_id
=
self
.
api_client
.
user_id
self
.
assert_responses_call_count
(
4
)
self
.
assertEqual
(
self
.
user_id
,
user_id
)
@responses.activate
def
test_user_id_failed
(
self
):
self
.
mock_login_response
(
200
)
self
.
mock_admin_response
(
200
)
self
.
mock_user_id_response
(
500
)
with
self
.
assertRaises
(
ProgramPublisherException
):
self
.
api_client
.
user_id
# pylint: disable=pointless-statement
@responses.activate
def
test_api_session
(
self
):
self
.
mock_login_response
(
200
)
self
.
mock_admin_response
(
200
)
self
.
mock_csrf_token_response
(
200
)
api_session
=
self
.
api_client
.
api_session
self
.
assert_responses_call_count
(
4
)
self
.
assertIsNotNone
(
api_session
)
self
.
assertEqual
(
api_session
.
headers
.
get
(
'Content-Type'
),
'application/json'
)
self
.
assertEqual
(
api_session
.
headers
.
get
(
'X-CSRF-Token'
),
self
.
csrf_token
)
@responses.activate
def
test_api_session_failed
(
self
):
self
.
mock_login_response
(
500
)
self
.
mock_admin_response
(
500
)
self
.
mock_csrf_token_response
(
500
)
with
self
.
assertRaises
(
ProgramPublisherException
):
self
.
api_client
.
api_session
# pylint: disable=pointless-statement
class
MarketingSitePublisherTests
(
MarketingSitePublisherTestMixin
):
class
MarketingSitePublisherTests
(
MarketingSitePublisherTestMixin
):
...
...
course_discovery/apps/course_metadata/tests/test_utils.py
View file @
f08645d2
...
@@ -2,8 +2,11 @@ import os
...
@@ -2,8 +2,11 @@ import os
import
ddt
import
ddt
from
django.test
import
TestCase
from
django.test
import
TestCase
import
responses
from
course_discovery.apps.course_metadata.exceptions
import
MarketingSiteAPIClientException
from
course_discovery.apps.course_metadata.tests.factories
import
ProgramFactory
from
course_discovery.apps.course_metadata.tests.factories
import
ProgramFactory
from
course_discovery.apps.course_metadata.tests.mixins
import
MarketingSiteAPIClientTestMixin
from
course_discovery.apps.course_metadata
import
utils
from
course_discovery.apps.course_metadata
import
utils
...
@@ -29,3 +32,84 @@ class UploadToFieldNamePathTests(TestCase):
...
@@ -29,3 +32,84 @@ class UploadToFieldNamePathTests(TestCase):
upload_path
=
upload_to
(
self
.
program
,
'name'
+
ext
)
upload_path
=
upload_to
(
self
.
program
,
'name'
+
ext
)
expected
=
os
.
path
.
join
(
path
,
str
(
getattr
(
self
.
program
,
field
))
+
ext
)
expected
=
os
.
path
.
join
(
path
,
str
(
getattr
(
self
.
program
,
field
))
+
ext
)
self
.
assertEqual
(
upload_path
,
expected
)
self
.
assertEqual
(
upload_path
,
expected
)
class
MarketingSiteAPIClientTests
(
MarketingSiteAPIClientTestMixin
):
"""
Unit test cases for MarketinSiteAPIClient
"""
def
setUp
(
self
):
super
(
MarketingSiteAPIClientTests
,
self
)
.
setUp
()
self
.
api_client
=
utils
.
MarketingSiteAPIClient
(
self
.
username
,
self
.
password
,
self
.
api_root
)
@responses.activate
def
test_init_session
(
self
):
self
.
mock_login_response
(
200
)
self
.
mock_admin_response
(
200
)
session
=
self
.
api_client
.
init_session
self
.
assert_responses_call_count
(
3
)
self
.
assertIsNotNone
(
session
)
@responses.activate
def
test_init_session_failed
(
self
):
self
.
mock_login_response
(
500
)
self
.
mock_admin_response
(
500
)
with
self
.
assertRaises
(
MarketingSiteAPIClientException
):
self
.
api_client
.
init_session
# pylint: disable=pointless-statement
@responses.activate
def
test_csrf_token
(
self
):
self
.
mock_login_response
(
200
)
self
.
mock_admin_response
(
200
)
self
.
mock_csrf_token_response
(
200
)
csrf_token
=
self
.
api_client
.
csrf_token
self
.
assert_responses_call_count
(
4
)
self
.
assertEqual
(
self
.
csrf_token
,
csrf_token
)
@responses.activate
def
test_csrf_token_failed
(
self
):
self
.
mock_login_response
(
200
)
self
.
mock_admin_response
(
200
)
self
.
mock_csrf_token_response
(
500
)
with
self
.
assertRaises
(
MarketingSiteAPIClientException
):
self
.
api_client
.
csrf_token
# pylint: disable=pointless-statement
@responses.activate
def
test_user_id
(
self
):
self
.
mock_login_response
(
200
)
self
.
mock_admin_response
(
200
)
self
.
mock_user_id_response
(
200
)
user_id
=
self
.
api_client
.
user_id
self
.
assert_responses_call_count
(
4
)
self
.
assertEqual
(
self
.
user_id
,
user_id
)
@responses.activate
def
test_user_id_failed
(
self
):
self
.
mock_login_response
(
200
)
self
.
mock_admin_response
(
200
)
self
.
mock_user_id_response
(
500
)
with
self
.
assertRaises
(
MarketingSiteAPIClientException
):
self
.
api_client
.
user_id
# pylint: disable=pointless-statement
@responses.activate
def
test_api_session
(
self
):
self
.
mock_login_response
(
200
)
self
.
mock_admin_response
(
200
)
self
.
mock_csrf_token_response
(
200
)
api_session
=
self
.
api_client
.
api_session
self
.
assert_responses_call_count
(
4
)
self
.
assertIsNotNone
(
api_session
)
self
.
assertEqual
(
api_session
.
headers
.
get
(
'Content-Type'
),
'application/json'
)
self
.
assertEqual
(
api_session
.
headers
.
get
(
'X-CSRF-Token'
),
self
.
csrf_token
)
@responses.activate
def
test_api_session_failed
(
self
):
self
.
mock_login_response
(
500
)
self
.
mock_admin_response
(
500
)
self
.
mock_csrf_token_response
(
500
)
with
self
.
assertRaises
(
MarketingSiteAPIClientException
):
self
.
api_client
.
api_session
# pylint: disable=pointless-statement
course_discovery/apps/course_metadata/utils.py
View file @
f08645d2
from
django.utils.functional
import
cached_property
import
requests
from
stdimage.models
import
StdImageFieldFile
from
stdimage.models
import
StdImageFieldFile
from
stdimage.utils
import
UploadTo
from
stdimage.utils
import
UploadTo
from
course_discovery.apps.course_metadata.exceptions
import
MarketingSiteAPIClientException
RESERVED_ELASTICSEARCH_QUERY_OPERATORS
=
(
'AND'
,
'OR'
,
'NOT'
,
'TO'
,)
RESERVED_ELASTICSEARCH_QUERY_OPERATORS
=
(
'AND'
,
'OR'
,
'NOT'
,
'TO'
,)
...
@@ -63,3 +67,71 @@ def custom_render_variations(file_name, variations, storage, replace=True):
...
@@ -63,3 +67,71 @@ def custom_render_variations(file_name, variations, storage, replace=True):
# to prevent default behaviour
# to prevent default behaviour
return
False
return
False
class
MarketingSiteAPIClient
(
object
):
"""
The marketing site API client we can use to communicate with the marketing site
"""
username
=
None
password
=
None
api_url
=
None
def
__init__
(
self
,
marketing_site_api_username
,
marketing_site_api_password
,
api_url
):
if
not
(
marketing_site_api_username
and
marketing_site_api_password
):
raise
MarketingSiteAPIClientException
(
'Marketing Site API credentials are not properly configured!'
)
self
.
username
=
marketing_site_api_username
self
.
password
=
marketing_site_api_password
self
.
api_url
=
api_url
.
strip
(
'/'
)
@cached_property
def
init_session
(
self
):
# Login to set session cookies
session
=
requests
.
Session
()
login_url
=
'{root}/user'
.
format
(
root
=
self
.
api_url
)
login_data
=
{
'name'
:
self
.
username
,
'pass'
:
self
.
password
,
'form_id'
:
'user_login'
,
'op'
:
'Log in'
,
}
response
=
session
.
post
(
login_url
,
data
=
login_data
)
expected_url
=
'{root}/users/{username}'
.
format
(
root
=
self
.
api_url
,
username
=
self
.
username
)
admin_url
=
'{root}/admin'
.
format
(
root
=
self
.
api_url
)
# Temporary way of checking whether the user has been logged into marketing site until
# the marketing site login flow is fixed
can_access_admin
=
session
.
get
(
admin_url
)
if
not
(
can_access_admin
.
status_code
==
200
and
response
.
url
==
expected_url
):
raise
MarketingSiteAPIClientException
(
'Marketing Site Login failed!'
)
return
session
@cached_property
def
api_session
(
self
):
self
.
init_session
.
headers
.
update
(
self
.
headers
)
return
self
.
init_session
@cached_property
def
csrf_token
(
self
):
token_url
=
'{root}/restws/session/token'
.
format
(
root
=
self
.
api_url
)
response
=
self
.
init_session
.
get
(
token_url
)
if
not
response
.
status_code
==
200
:
raise
MarketingSiteAPIClientException
(
'Failed to retrieve Marketing Site CSRF token!'
)
token
=
response
.
content
.
decode
(
'utf8'
)
return
token
@cached_property
def
user_id
(
self
):
# Get a user ID
user_url
=
'{root}/user.json?name={username}'
.
format
(
root
=
self
.
api_url
,
username
=
self
.
username
)
response
=
self
.
init_session
.
get
(
user_url
)
if
not
response
.
status_code
==
200
:
raise
MarketingSiteAPIClientException
(
'Failed to retrieve Marketing site user details!'
)
user_id
=
response
.
json
()[
'list'
][
0
][
'uid'
]
return
user_id
@cached_property
def
headers
(
self
):
return
{
'Content-Type'
:
'application/json'
,
'X-CSRF-Token'
:
self
.
csrf_token
,
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment