Previously, an error was raised if the comments service returned data including an unexpected field, which unnecessarily complicated the release path for new features, since the list of allowed fields would need to be modified before cs_comments_service could be modified, and only then could edx-platform take advantage of the new CS feature. We still log a warning if an unexpected field is returned, so we will still be able to tell if the CS returns a corrupt response. JIRA: FOR-180
178 lines
5.9 KiB
Python
178 lines
5.9 KiB
Python
import logging
|
|
|
|
from .utils import extract, perform_request, CommentClientRequestError
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class Model(object):
|
|
|
|
accessible_fields = ['id']
|
|
updatable_fields = ['id']
|
|
initializable_fields = ['id']
|
|
base_url = None
|
|
default_retrieve_params = {}
|
|
metric_tag_fields = []
|
|
|
|
DEFAULT_ACTIONS_WITH_ID = ['get', 'put', 'delete']
|
|
DEFAULT_ACTIONS_WITHOUT_ID = ['get_all', 'post']
|
|
DEFAULT_ACTIONS = DEFAULT_ACTIONS_WITH_ID + DEFAULT_ACTIONS_WITHOUT_ID
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.attributes = extract(kwargs, self.accessible_fields)
|
|
self.retrieved = False
|
|
|
|
def __getattr__(self, name):
|
|
if name == 'id':
|
|
return self.attributes.get('id', None)
|
|
try:
|
|
return self.attributes[name]
|
|
except KeyError:
|
|
if self.retrieved or self.id is None:
|
|
raise AttributeError("Field {0} does not exist".format(name))
|
|
self.retrieve()
|
|
return self.__getattr__(name)
|
|
|
|
def __setattr__(self, name, value):
|
|
if name == 'attributes' or name not in self.accessible_fields:
|
|
super(Model, self).__setattr__(name, value)
|
|
else:
|
|
self.attributes[name] = value
|
|
|
|
def __getitem__(self, key):
|
|
if key not in self.accessible_fields:
|
|
raise KeyError("Field {0} does not exist".format(key))
|
|
return self.attributes.get(key)
|
|
|
|
def __setitem__(self, key, value):
|
|
if key not in self.accessible_fields:
|
|
raise KeyError("Field {0} does not exist".format(key))
|
|
self.attributes.__setitem__(key, value)
|
|
|
|
def items(self, *args, **kwargs):
|
|
return self.attributes.items(*args, **kwargs)
|
|
|
|
def get(self, *args, **kwargs):
|
|
return self.attributes.get(*args, **kwargs)
|
|
|
|
def to_dict(self):
|
|
self.retrieve()
|
|
return self.attributes
|
|
|
|
def retrieve(self, *args, **kwargs):
|
|
if not self.retrieved:
|
|
self._retrieve(*args, **kwargs)
|
|
self.retrieved = True
|
|
return self
|
|
|
|
def _retrieve(self, *args, **kwargs):
|
|
url = self.url(action='get', params=self.attributes)
|
|
response = perform_request(
|
|
'get',
|
|
url,
|
|
self.default_retrieve_params,
|
|
metric_tags=self._metric_tags,
|
|
metric_action='model.retrieve'
|
|
)
|
|
self._update_from_response(response)
|
|
|
|
@property
|
|
def _metric_tags(self):
|
|
"""
|
|
Returns a list of tags to be used when recording metrics about this model.
|
|
|
|
Each field named in ``self.metric_tag_fields`` is used as a tag value,
|
|
under the key ``<class>.<metric_field>``. The tag model_class is used to
|
|
record the class name of the model.
|
|
"""
|
|
tags = [
|
|
u'{}.{}:{}'.format(self.__class__.__name__, attr, self[attr])
|
|
for attr in self.metric_tag_fields
|
|
if attr in self.attributes
|
|
]
|
|
tags.append(u'model_class:{}'.format(self.__class__.__name__))
|
|
return tags
|
|
|
|
@classmethod
|
|
def find(cls, id):
|
|
return cls(id=id)
|
|
|
|
def _update_from_response(self, response_data):
|
|
for k, v in response_data.items():
|
|
if k in self.accessible_fields:
|
|
self.__setattr__(k, v)
|
|
else:
|
|
log.warning(
|
|
"Unexpected field {field_name} in model {model_name}".format(
|
|
field_name=k,
|
|
model_name=self.__class__.__name__
|
|
)
|
|
)
|
|
|
|
def updatable_attributes(self):
|
|
return extract(self.attributes, self.updatable_fields)
|
|
|
|
def initializable_attributes(self):
|
|
return extract(self.attributes, self.initializable_fields)
|
|
|
|
@classmethod
|
|
def before_save(cls, instance):
|
|
pass
|
|
|
|
@classmethod
|
|
def after_save(cls, instance):
|
|
pass
|
|
|
|
def save(self):
|
|
self.before_save(self)
|
|
if self.id: # if we have id already, treat this as an update
|
|
url = self.url(action='put', params=self.attributes)
|
|
response = perform_request(
|
|
'put',
|
|
url,
|
|
self.updatable_attributes(),
|
|
metric_tags=self._metric_tags,
|
|
metric_action='model.update'
|
|
)
|
|
else: # otherwise, treat this as an insert
|
|
url = self.url(action='post', params=self.attributes)
|
|
response = perform_request(
|
|
'post',
|
|
url,
|
|
self.initializable_attributes(),
|
|
metric_tags=self._metric_tags,
|
|
metric_action='model.insert'
|
|
)
|
|
self.retrieved = True
|
|
self._update_from_response(response)
|
|
self.after_save(self)
|
|
|
|
def delete(self):
|
|
url = self.url(action='delete', params=self.attributes)
|
|
response = perform_request('delete', url, metric_tags=self._metric_tags, metric_action='model.delete')
|
|
self.retrieved = True
|
|
self._update_from_response(response)
|
|
|
|
@classmethod
|
|
def url_with_id(cls, params={}):
|
|
return cls.base_url + '/' + str(params['id'])
|
|
|
|
@classmethod
|
|
def url_without_id(cls, params={}):
|
|
return cls.base_url
|
|
|
|
@classmethod
|
|
def url(cls, action, params={}):
|
|
if cls.base_url is None:
|
|
raise CommentClientRequestError("Must provide base_url when using default url function")
|
|
if action not in cls.DEFAULT_ACTIONS:
|
|
raise ValueError("Invalid action {0}. The supported action must be in {1}".format(action, str(cls.DEFAULT_ACTIONS)))
|
|
elif action in cls.DEFAULT_ACTIONS_WITH_ID:
|
|
try:
|
|
return cls.url_with_id(params)
|
|
except KeyError:
|
|
raise CommentClientRequestError("Cannot perform action {0} without id".format(action))
|
|
else: # action must be in DEFAULT_ACTIONS_WITHOUT_ID now
|
|
return cls.url_without_id()
|