-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathengagement.py
More file actions
370 lines (320 loc) · 14.6 KB
/
Copy pathengagement.py
File metadata and controls
370 lines (320 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
"""Engagement model class.
Manages the engagement
"""
from __future__ import annotations
import logging
from datetime import datetime
from typing import List, Optional
from sqlalchemy import and_, asc, desc, or_
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy.sql import text
from sqlalchemy.sql.schema import ForeignKey
from met_api.constants.engagement_status import EngagementDisplayStatus, Status
from met_api.constants.user import SYSTEM_USER
from met_api.models.engagement_scope_options import EngagementScopeOptions
from met_api.models.membership import Membership as MembershipModel
from met_api.models.pagination_options import PaginationOptions
from met_api.models.staff_user import StaffUser
from met_api.schemas.engagement import EngagementSchema
from met_api.utils.datetime import local_datetime
from met_api.utils.enums import MembershipStatus
from met_api.utils.filter_types import filter_map
from .base_model import BaseModel
from .db import db
from .engagement_metadata import EngagementMetadata as EngagementMetadataModel
from .engagement_status import EngagementStatus
class Engagement(BaseModel):
"""Definition of the Engagement entity."""
__tablename__ = 'engagement'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(50))
description = db.Column(db.Text, unique=False, nullable=False)
rich_description = db.Column(JSON, unique=False, nullable=False)
description_title = db.Column(db.String(255), unique=False, nullable=False)
start_date = db.Column(db.DateTime)
end_date = db.Column(db.DateTime)
status_id = db.Column(db.Integer, ForeignKey(
'engagement_status.id', ondelete='CASCADE'))
status = db.relationship('EngagementStatus', backref='engagement', viewonly=True)
published_date = db.Column(db.DateTime, nullable=True)
scheduled_date = db.Column(db.DateTime, nullable=True)
banner_filename = db.Column(db.String(), unique=False, nullable=True)
surveys = db.relationship(
'Survey', backref='engagement', cascade='all, delete')
status_block = db.relationship(
'EngagementStatusBlock', backref='engagement')
tenant_id = db.Column(
db.Integer, db.ForeignKey('tenant.id'), nullable=True)
tenant = db.relationship('Tenant', backref='engagements')
is_internal = db.Column(db.Boolean, nullable=False)
consent_message = db.Column(JSON, unique=False, nullable=True)
sponsor_name = db.Column(db.String(50), nullable=True)
@classmethod
def get_engagements_paginated(
cls,
external_user_id,
pagination_options: PaginationOptions,
scope_options: EngagementScopeOptions,
search_options=None,
):
"""Get engagements paginated."""
query = db.session.query(Engagement).join(EngagementStatus)
query = cls._add_tenant_filter(query)
if search_options:
query = cls._filter_by_search_text(query, search_options)
query = cls._filter_by_created_date(query, search_options)
query = cls._filter_by_published_date(query, search_options)
query = cls._filter_by_engagement_status(query, search_options)
query = cls._filter_by_metadata(query, search_options)
query = cls._filter_by_internal(query, search_options)
if scope_options.restricted:
if scope_options.include_assigned:
# the engagement status ids that should not be filtered out
exception_status_ids = scope_options.engagement_status_ids
query = cls._filter_by_assigned_engagements(
query, external_user_id, exception_status_ids)
else:
# the engagement status ids of the engagements that should fetched
statuses = scope_options.engagement_status_ids
query = cls._filter_by_statuses(query, statuses)
sort = cls._get_sort_order(pagination_options)
query = query.order_by(sort)
no_pagination_options = not pagination_options.page or not pagination_options.size
if no_pagination_options:
items = query.all()
return items, len(items)
# Calculate offset and limit for pagination
offset = (pagination_options.page - 1) * pagination_options.size
limit = pagination_options.size
# Apply pagination using limit and offset
paginated_query = query.offset(offset).limit(limit)
# Fetch paginated items and total count
items = paginated_query.all()
total_count = query.count()
return items, total_count
@classmethod
def update_engagement(cls, engagement: EngagementSchema) -> Engagement:
"""Update engagement."""
engagement_id = engagement.get('id', None)
query = Engagement.query.filter_by(id=engagement_id)
record: Engagement = query.first()
if not record:
return None
update_fields = {
'name': engagement.get('name', None),
'description': engagement.get('description', None),
'rich_description': engagement.get('rich_description', None),
'description_title': engagement.get('description_title', None),
'start_date': engagement.get('start_date', None),
'end_date': engagement.get('end_date', None),
'status_id': engagement.get('status_id', None),
# to fix the bug with UI not passing published date always.
# Defaulting to existing
'published_date': engagement.get(
'published_date', record.published_date),
'scheduled_date': engagement.get(
'scheduled_date', record.scheduled_date),
'updated_date': datetime.utcnow(),
'updated_by': engagement.get('updated_by', None),
'banner_filename': engagement.get('banner_filename', None),
'is_internal': engagement.get('is_internal', record.is_internal),
'consent_message': engagement.get(
'consent_message', record.consent_message),
'sponsor_name': engagement.get('sponsor_name', record.sponsor_name),
'cta_url': engagement.get('cta_url', record.cta_url),
}
query.update(update_fields)
db.session.commit()
return record
@classmethod
def edit_engagement(cls, engagement_data: dict) -> Optional[Engagement]:
"""Update engagement."""
engagement_id = engagement_data.get('id', None)
query = Engagement.query.filter_by(id=engagement_id)
engagement: Engagement = query.first()
if not engagement:
return None
engagement_data['updated_date'] = datetime.utcnow()
query.update(engagement_data)
db.session.commit()
return engagement
@classmethod
def find_tenant_id_by_id(cls, engagement_id):
"""Return the tenant id for the engagement."""
return db.session.query(cls.tenant_id).filter_by(id=engagement_id).scalar()
@classmethod
def close_engagements_due(cls) -> List[Engagement]:
"""Update engagement to closed."""
now = local_datetime()
# Strip the time off the datetime object
date_due = datetime(now.year, now.month, now.day)
update_fields = {
'status_id': Status.Closed.value,
'updated_date': datetime.utcnow(),
'updated_by': SYSTEM_USER
}
# Close published engagements where end date is prior than today
query = Engagement.query \
.filter(Engagement.status_id == Status.Published.value) \
.filter(Engagement.end_date < date_due)
records = query.all()
if not records:
return []
query.update(update_fields)
db.session.commit()
return records
@classmethod
def publish_scheduled_engagements_due(cls) -> List[Engagement]:
"""Update scheduled engagements to published."""
datetime_due = datetime.utcnow()
logging.getLogger(__name__).debug('Publish due date (UTC): %s', datetime_due)
update_fields = {
'status_id': Status.Published.value,
'published_date': datetime.utcnow(),
'updated_date': datetime.utcnow(),
'updated_by': SYSTEM_USER
}
# Publish scheduled engagements where scheduled datetime is prior than now
query = Engagement.query \
.filter(Engagement.status_id == Status.Scheduled.value) \
.filter(Engagement.scheduled_date <= datetime_due)
records = query.all()
if not records:
return None
query.update(update_fields)
db.session.commit()
return records
@staticmethod
def _get_sort_order(pagination_options):
sort = asc(text(pagination_options.sort_key)) if pagination_options.sort_order == 'asc' \
else desc(text(pagination_options.sort_key))
return sort
@staticmethod
def _filter_by_engagement_status(query, search_options):
statuses = [int(status)
for status in search_options.get('engagement_status', [])]
if not statuses:
return query
allowed_statuses = [
Status.Draft.value,
Status.Published.value,
Status.Closed.value,
Status.Scheduled.value
]
status_filter = [Engagement.status_id.in_(
[status for status in statuses if status in allowed_statuses])]
if EngagementDisplayStatus.Upcoming.value in statuses:
status_filter.append(
and_(
Engagement.status_id == Status.Published.value,
Engagement.start_date > datetime.now()
)
)
if EngagementDisplayStatus.Open.value in statuses:
status_filter.append(
and_(
Engagement.status_id == Status.Published.value,
Engagement.start_date <= datetime.now()
)
)
if EngagementDisplayStatus.Unpublished.value in statuses:
status_filter.append(Engagement.status_id ==
Status.Unpublished.value)
query = query.filter(or_(*status_filter))
return query
@classmethod
def _filter_by_published_date(cls, query, search_options):
if published_from_date := search_options.get('published_from_date'):
query = query.filter(
Engagement.published_date >= published_from_date)
if published_to_date := search_options.get('published_to_date'):
query = query.filter(
Engagement.published_date <= published_to_date)
return query
@staticmethod
def _filter_by_created_date(query, search_options):
if created_from_date := search_options.get('created_from_date'):
query = query.filter(Engagement.created_date >= created_from_date)
if created_to_date := search_options.get('created_to_date'):
query = query.filter(Engagement.created_date <= created_to_date)
return query
@staticmethod
def _filter_by_search_text(query, search_options):
if search_text := search_options.get('search_text'):
query = query.filter(
Engagement.name.ilike('%' + search_text + '%'))
return query
@staticmethod
def _filter_by_internal(query, search_options):
if exclude_internal := search_options.get('exclude_internal'):
if exclude_internal:
query = query.filter(Engagement.is_internal.is_(False))
return query
@staticmethod
def _filter_by_metadata(query, search_options):
"""
Filter the engagements based on metadata criteria.
Ensures that each engagement matches all of the provided criteria.
"""
if 'metadata' not in search_options:
return query
for criterion in search_options['metadata']:
taxon_id = criterion.get('taxon_id')
values = criterion.get('values')
# pick the type of filtering to apply
filter_type = filter_map.get(criterion.get('filter_type'))
if any([taxon_id is None, values is None, filter_type is None]):
continue # skip criterion if any of the required fields are missing
taxon_query = query.session.query(
EngagementMetadataModel.engagement_id
).filter(
# Filter the metadata entries to only include those that match the current taxon
EngagementMetadataModel.taxon_id == taxon_id
)
# Use the filter function to create a subquery that filters the engagements
filter_subquery = filter_type(taxon_query, values)
# Filter the main query to include only engagements found in the subquery
query = query.filter(Engagement.id.in_(filter_subquery))
return query
@staticmethod
def _filter_by_assigned_engagements(query, external_user_id: int, exception_status_ids: Optional[list[int]] = None):
if exception_status_ids is None:
exception_status_ids = []
assigned_engagement_ids = [
engagement_id
for engagement_id, in (
db.session.query(Engagement.id)
.join(MembershipModel, MembershipModel.engagement_id == Engagement.id)
.join(StaffUser, StaffUser.external_id == external_user_id)
.filter(MembershipModel.user_id == StaffUser.id)
.filter(MembershipModel.is_latest.is_(True))
.filter(MembershipModel.status == MembershipStatus.ACTIVE.value)
.all()
)
]
# filter out all engagements that are not assigned to the user except ones with the exception status ids
query = query.filter(
or_(
Engagement.status_id.in_(exception_status_ids),
Engagement.id.in_(assigned_engagement_ids)
)
)
return query
# filter by statuses
@staticmethod
def _filter_by_statuses(query, statuses: list[int]):
query = query.filter(Engagement.status_id.in_(statuses))
return query
@staticmethod
def get_assigned_engagements(user_id: int) -> List[Engagement]:
"""Get engagements assigned to the given user id."""
engagements = db.session.query(Engagement) \
.join(MembershipModel) \
.filter(
and_(
MembershipModel.user_id == user_id,
MembershipModel.is_latest.is_(True),
MembershipModel.status == MembershipStatus.ACTIVE.value
)) \
.all()
return engagements