Coverage for portality/models/v2/application.py: 79%
207 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-09-21 00:49 +0100
« prev ^ index » next coverage.py v6.4.2, created at 2022-09-21 00:49 +0100
1from copy import deepcopy
3from portality import constants
4from portality.core import app
5from portality.lib import es_data_mapping
6from portality.models.v2 import shared_structs
7from portality.models.v2.journal import JournalLikeObject, Journal
8from portality.lib.coerce import COERCE_MAP
9from portality.dao import DomainObject
11APPLICATION_STRUCT = {
12 "objects": [
13 "admin", "index"
14 ],
16 "structs": {
17 "admin": {
18 "fields": {
19 "current_journal": {"coerce": "unicode"},
20 "related_journal": {"coerce": "unicode"},
21 "application_status": {"coerce": "unicode"},
22 "date_applied": {"coerce": "utcdatetime"},
23 "application_type": {
24 "coerce": "unicode",
25 "allowed_values": [
26 constants.APPLICATION_TYPE_NEW_APPLICATION,
27 constants.APPLICATION_TYPE_UPDATE_REQUEST
28 ]
29 }
30 }
31 },
32 "index": {
33 "fields": {
34 "application_type": {"coerce": "unicode"}
35 }
36 }
37 }
38}
41class Application(JournalLikeObject):
42 __type__ = "application"
44 __SEAMLESS_STRUCT__ = [
45 shared_structs.JOURNAL_BIBJSON,
46 shared_structs.SHARED_JOURNAL_LIKE,
47 APPLICATION_STRUCT
48 ]
50 __SEAMLESS_COERCE__ = COERCE_MAP
52 def __init__(self, **kwargs):
53 # FIXME: hack, to deal with ES integration layer being improperly abstracted
54 if "_source" in kwargs:
55 kwargs = kwargs["_source"]
56 super(Application, self).__init__(raw=kwargs)
57 if self.application_type is None:
58 if self.current_journal:
59 self.set_is_update_request(True)
60 else:
61 self.set_is_update_request(False)
63 @classmethod
64 def get_by_owner(cls, owner):
65 q = SuggestionQuery(owner=owner)
66 result = cls.query(q=q.query())
67 records = [cls(**r.get("_source")) for r in result.get("hits", {}).get("hits", [])]
68 return records
70 @classmethod
71 def delete_selected(cls, email=None, statuses=None):
72 q = SuggestionQuery(email=email, statuses=statuses)
73 r = cls.delete_by_query(q.query())
75 @classmethod
76 def list_by_status(cls, status):
77 q = StatusQuery(status)
78 return cls.iterate(q=q.query())
80 @classmethod
81 def find_latest_by_current_journal(cls, journal_id):
82 q = CurrentJournalQuery(journal_id)
83 results = cls.q2obj(q=q.query())
84 if len(results) > 0:
85 return results[0]
86 return None
88 @classmethod
89 def find_all_by_related_journal(cls, journal_id):
90 q = RelatedJournalQuery(journal_id, size=1000)
91 return cls.q2obj(q=q.query())
93 @classmethod
94 def assignment_to_editor_groups(cls, egs):
95 q = AssignedEditorGroupsQuery([eg.name for eg in egs])
96 res = cls.query(q.query())
97 buckets = res.get("aggregations", {}).get("editor_groups", {}).get("buckets", [])
98 assignments = {}
99 for b in buckets:
100 assignments[b.get("key")] = b.get("doc_count")
101 return assignments
103 def mappings(self):
104 return es_data_mapping.create_mapping(self.__seamless_struct__.raw, MAPPING_OPTS)
106 @property
107 def application_type(self):
108 return self.__seamless__.get_single("admin.application_type")
110 @application_type.setter
111 def application_type(self, val):
112 self.__seamless__.set_with_struct("admin.application_type", val)
114 def set_is_update_request(self, val):
115 application_type = constants.APPLICATION_TYPE_UPDATE_REQUEST if val is True else constants.APPLICATION_TYPE_NEW_APPLICATION
116 self.application_type = application_type
118 @property
119 def current_journal(self):
120 return self.__seamless__.get_single("admin.current_journal")
122 def set_current_journal(self, journal_id):
123 # anything that has a current journal is, by definition, an update request
124 self.set_is_update_request(True)
125 self.__seamless__.set_with_struct("admin.current_journal", journal_id)
127 def remove_current_journal(self):
128 self.__seamless__.delete("admin.current_journal")
130 @property
131 def related_journal(self):
132 return self.__seamless__.get_single("admin.related_journal")
134 def set_related_journal(self, journal_id):
135 self.__seamless__.set_with_struct("admin.related_journal", journal_id)
137 def remove_related_journal(self):
138 self.__seamless__.delete("admin.related_journal")
140 @property
141 def related_journal_object(self):
142 if self.related_journal:
143 return Journal.pull(self.related_journal)
144 return None
146 @property
147 def application_status(self):
148 return self.__seamless__.get_single("admin.application_status")
150 def set_application_status(self, val):
151 self.__seamless__.set_with_struct("admin.application_status", val)
153 @property
154 def date_applied(self):
155 return self.__seamless__.get_single("admin.date_applied")
157 @date_applied.setter
158 def date_applied(self, val):
159 self.__seamless__.set_with_struct("admin.date_applied", val)
161 def _sync_owner_to_journal(self):
162 if self.current_journal is None:
163 return
164 from portality.models.v2.journal import Journal
165 cj = Journal.pull(self.current_journal)
166 if cj is not None and cj.owner != self.owner:
167 cj.set_owner(self.owner)
168 cj.save(sync_owner=False)
170 def _generate_index(self):
171 super(Application, self)._generate_index()
173 # index_record_type = None
174 # if self.application_type == constants.APPLICATION_TYPE_NEW_APPLICATION:
175 # if self.application_status in [constants.APPLICATION_STATUS_REJECTED, constants.APPLICATION_STATUS_ACCEPTED]:
176 # index_record_type = constants.INDEX_RECORD_TYPE_NEW_APPLICATION_FINISHED
177 # else:
178 # index_record_type = constants.INDEX_RECORD_TYPE_NEW_APPLICATION_UNFINISHED
179 # elif self.application_type == constants.APPLICATION_TYPE_UPDATE_REQUEST:
180 # if self.application_status in [constants.APPLICATION_STATUS_REJECTED, constants.APPLICATION_STATUS_ACCEPTED]:
181 # index_record_type = constants.INDEX_RECORD_TYPE_UPDATE_REQUEST_FINISHED
182 # else:
183 # index_record_type = constants.INDEX_RECORD_TYPE_UPDATE_REQUEST_UNFINISHED
184 # if index_record_type is not None:
185 # self.__seamless__.set_with_struct("index.application_type", index_record_type)
187 # FIXME: Temporary partial reversion of an indexing change (this index.application_type data is still being used
188 # in applications search)
189 if self.current_journal is not None:
190 self.__seamless__.set_with_struct("index.application_type", "update request")
191 elif self.application_status in [constants.APPLICATION_STATUS_ACCEPTED, constants.APPLICATION_STATUS_REJECTED]:
192 self.__seamless__.set_with_struct("index.application_type", "finished application/update")
193 else:
194 self.__seamless__.set_with_struct("index.application_type", "new application")
196 def prep(self, is_update=True):
197 self._generate_index()
198 if is_update:
199 self.set_last_updated()
201 def save(self, sync_owner=True, **kwargs):
202 self.prep()
203 self.verify_against_struct()
204 if sync_owner:
205 self._sync_owner_to_journal()
206 return super(Application, self).save(**kwargs)
208 #########################################################
209 ## DEPRECATED METHODS
211 @property
212 def suggested_on(self):
213 return self.date_applied
215 @suggested_on.setter
216 def suggested_on(self, val):
217 self.date_applied = val
219 @property
220 def suggester(self):
221 owner = self.owner
222 if owner is None:
223 return None
224 from portality.models import Account
225 oacc = Account.pull(owner)
226 if oacc is None:
227 return None
228 return {
229 "name": owner,
230 "email": oacc.email
231 }
234class DraftApplication(Application):
235 __type__ = "draft_application"
237 __SEAMLESS_APPLY_STRUCT_ON_INIT__ = False
238 __SEAMLESS_CHECK_REQUIRED_ON_INIT__ = False
241class AllPublisherApplications(DomainObject):
242 __type__ = "draft_application,application"
245MAPPING_OPTS = {
246 "dynamic": None,
247 "coerces": app.config["DATAOBJ_TO_MAPPING_DEFAULTS"],
248 "exceptions": {
249 "admin.notes.note": {
250 "type": "text",
251 "index": False,
252 # "include_in_all": False # Removed in es6 fixme: do we need to look at copy_to for the mapping?
253 }
254 }
255}
258class SuggestionQuery(object):
259 _base_query = {"track_total_hits": True, "query": {"bool": {"must": []}}}
260 _email_term = {"term": {"admin.applicant.email.exact": "<email address>"}}
261 _status_terms = {"terms": {"admin.application_status.exact": ["<list of statuses>"]}}
262 _owner_term = {"term": {"admin.owner.exact": "<the owner id>"}}
264 def __init__(self, email=None, statuses=None, owner=None):
265 self.email = email
266 self.owner = owner
267 if statuses:
268 self.statuses = statuses
269 else:
270 self.statuses = []
272 def query(self):
273 q = deepcopy(self._base_query)
274 if self.email:
275 et = deepcopy(self._email_term)
276 et["term"]["admin.applicant.email.exact"] = self.email
277 q["query"]["bool"]["must"].append(et)
278 if self.statuses and len(self.statuses) > 0:
279 st = deepcopy(self._status_terms)
280 st["terms"]["admin.application_status.exact"] = self.statuses
281 q["query"]["bool"]["must"].append(st)
283 if self.owner is not None:
284 ot = deepcopy(self._owner_term)
285 ot["term"]["admin.owner.exact"] = self.owner
286 q["query"]["bool"]["must"].append(ot)
287 return q
290class OwnerStatusQuery(object):
291 base_query = {
292 "track_total_hits": True,
293 "query": {
294 "bool": {
295 "must": []
296 }
297 },
298 "sort": [
299 {"created_date": "desc"}
300 ],
301 "size": 10
302 }
304 def __init__(self, owner, statuses, size=10):
305 self._query = deepcopy(self.base_query)
306 owner_term = {"match": {"owner": owner}}
307 self._query["query"]["bool"]["must"].append(owner_term)
308 status_term = {"terms": {"admin.application_status.exact": statuses}}
309 self._query["query"]["bool"]["must"].append(status_term)
310 self._query["size"] = size
312 def query(self):
313 return self._query
316class StatusQuery(object):
318 def __init__(self, statuses):
319 if not isinstance(statuses, list):
320 statuses = [statuses]
321 self.statuses = statuses
323 def query(self):
324 return {
325 "track_total_hits": True,
326 "query": {
327 "bool": {
328 "must": [
329 {"terms": {"admin.application_status.exact": self.statuses}}
330 ]
331 }
332 }
333 }
336class CurrentJournalQuery(object):
338 def __init__(self, journal_id, size=1):
339 self.journal_id = journal_id
340 self.size = size
342 def query(self):
343 return {
344 "track_total_hits": True,
345 "query": {
346 "bool": {
347 "must": [
348 {"term": {"admin.current_journal.exact": self.journal_id}}
349 ]
350 }
351 },
352 "sort": [
353 {"created_date": {"order": "desc"}}
354 ],
355 "size": self.size
356 }
359class RelatedJournalQuery(object):
361 def __init__(self, journal_id, size=1):
362 self.journal_id = journal_id
363 self.size = size
365 def query(self):
366 return {
367 "track_total_hits": True,
368 "query": {
369 "bool": {
370 "must": [
371 {"term": {"admin.related_journal.exact": self.journal_id}}
372 ]
373 }
374 },
375 "sort": [
376 {"created_date": {"order": "asc"}}
377 ],
378 "size": self.size
379 }
382class AssignedEditorGroupsQuery(object):
383 """
384 ~~->$AssignedEditorGroups:Query~~
385 ~~^->Elasticsearch:Technology~~
386 """
388 def __init__(self, editor_groups, application_type_name="new application"):
389 self.editor_groups = editor_groups
390 self.application_type_name = application_type_name
392 def query(self):
393 return {
394 "query": {
395 "bool": {
396 "must": [
397 {"terms": {"admin.editor_group.exact": self.editor_groups}},
398 {"term": {"index.application_type.exact": self.application_type_name}}
399 ]
400 }
401 },
402 "size": 0,
403 "aggs": {
404 "editor_groups": {
405 "terms": {
406 "field": "admin.editor_group.exact",
407 "size": len(self.editor_groups) or 1,
408 "order": {"_term": "asc"}
409 }
410 }
411 }
412 }