Skip to content

Commit e7de42c

Browse files
authored
Merge pull request #524 from mapswipe/automatically-update-project-status
Automatically update project status
2 parents 18abeaa + 997ec11 commit e7de42c

File tree

3 files changed

+136
-0
lines changed

3 files changed

+136
-0
lines changed

mapswipe_workers/mapswipe_workers/definitions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,14 @@ class CustomError(Exception):
109109

110110

111111
class MessageType(Enum):
112+
"""The different types of messages sent via Slack."""
113+
112114
SUCCESS = 1
113115
FAIL = 2
114116
NOTIFICATION_90 = 3
115117
NOTIFICATION_100 = 4
118+
PROJECT_STATUS_FINISHED = 5
119+
PROJECT_STATUS_ACTIVE = 6
116120

117121

118122
class ProjectType(Enum):

mapswipe_workers/mapswipe_workers/utils/slack_helper.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,24 @@ def send_slack_message(
5858
+ "and activate another one."
5959
)
6060
slack_client.chat_postMessage(channel="mapswipe_managers", text=message)
61+
elif message_type == MessageType.PROJECT_STATUS_FINISHED:
62+
message = (
63+
"### SET PROJECT STATUS TO FINISHED ###\n"
64+
+ f"Project Name: {project_name}\n"
65+
+ f"Project Id: {project_id}\n\n"
66+
+ "The status of the project has been set to 'finished' "
67+
+ "by MapSwipe's backend workers."
68+
)
69+
slack_client.chat_postMessage(channel=SLACK_CHANNEL, text=message)
70+
elif message_type == MessageType.PROJECT_STATUS_ACTIVE:
71+
message = (
72+
"### SET PROJECT STATUS TO ACTIVE ###\n"
73+
+ f"Project Name: {project_name}\n"
74+
+ f"Project Id: {project_id}\n\n"
75+
+ "The status of the project has been set to 'active' "
76+
+ "by MapSwipe's backend workers."
77+
)
78+
slack_client.chat_postMessage(channel=SLACK_CHANNEL, text=message)
6179
else:
6280
# TODO: Raise an Exception
6381
pass
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import sys
2+
import time
3+
from typing import List, OrderedDict
4+
5+
import schedule as sched
6+
7+
from mapswipe_workers.auth import firebaseDB
8+
from mapswipe_workers.definitions import MessageType, logger
9+
from mapswipe_workers.utils.slack_helper import send_slack_message
10+
11+
12+
def get_projects(status: str) -> OrderedDict:
13+
"""Load 'active' projects from Firebase."""
14+
fb_db = firebaseDB()
15+
projects = (
16+
fb_db.reference("v2/projects/").order_by_child("status").equal_to(status).get()
17+
)
18+
logger.info(f"got {len(projects)} {status} projects from firebase")
19+
return projects
20+
21+
22+
def filter_projects_by_name_and_progress(
23+
projects: OrderedDict, filter_string: str, progress_threshold: int
24+
) -> List[str]:
25+
"""Filter projects by name (lowercase) and progress."""
26+
selected_project_ids = []
27+
for project_id in projects.keys():
28+
name = projects[project_id]["name"]
29+
progress = projects[project_id]["progress"]
30+
if filter_string.lower() in name.lower() and progress >= progress_threshold:
31+
selected_project_ids.append(project_id)
32+
33+
logger.info(
34+
f"selected {len(selected_project_ids)} project(s) which contain(s) "
35+
f"'{filter_string}' in the project name and progress >= {progress_threshold}%."
36+
)
37+
return selected_project_ids
38+
39+
40+
def set_status_in_firebase(project_id: str, project_name: str, new_status: str) -> None:
41+
"""Change status of a project in Firebase."""
42+
# change status in firebase
43+
fb_db = firebaseDB()
44+
fb_db.reference(f"v2/projects/{project_id}/status").set(new_status)
45+
logger.info(f"set project status to '{new_status}' for project {project_id}")
46+
47+
# send slack message
48+
if new_status == "finished":
49+
send_slack_message(
50+
MessageType.PROJECT_STATUS_FINISHED, project_name, project_id
51+
)
52+
elif new_status == "active":
53+
send_slack_message(MessageType.PROJECT_STATUS_ACTIVE, project_name, project_id)
54+
55+
56+
def run_update_project_status(filter_string: str) -> None:
57+
"""Run the workflow to update project status for all filtered projects."""
58+
logger.info("### Start update project status workflow ###")
59+
active_projects = get_projects(status="active")
60+
finished_projects = filter_projects_by_name_and_progress(
61+
active_projects, filter_string, progress_threshold=100,
62+
)
63+
64+
inactive_projects = get_projects(status="inactive")
65+
# We sort projects by their attribute "projectNumber" to ensure that
66+
# always the lowest one will be set to "status=active" next.
67+
inactive_projects = OrderedDict(
68+
sorted(inactive_projects.items(), key=lambda x: x[1]["projectNumber"])
69+
)
70+
71+
new_active_projects = filter_projects_by_name_and_progress(
72+
inactive_projects, filter_string, progress_threshold=0,
73+
)[0 : len(finished_projects)]
74+
75+
# Here we check that there is at least one inactive project
76+
# which can be activated in the app.
77+
# We do this to avoid that there is no project left in the app.
78+
if len(new_active_projects) > 0:
79+
for project_id in finished_projects:
80+
project_name = active_projects[project_id]["name"]
81+
set_status_in_firebase(project_id, project_name, new_status="finished")
82+
83+
for project_id in new_active_projects:
84+
project_name = inactive_projects[project_id]["name"]
85+
set_status_in_firebase(project_id, project_name, new_status="active")
86+
logger.info("### Finished update project status workflow ###")
87+
88+
89+
if __name__ == "__main__":
90+
"""Use this command to run in docker container.
91+
docker-compose run -d mapswipe_workers_creation python3 python_scripts/update_project_status.py "test" 30 # noqa
92+
93+
You need to use two arguments for the script
94+
- filter string, e.g. "test"
95+
- time interval in minutes, e.g. 30
96+
97+
Make sure that you don't run this script too frequently as it pulls data from firebase and
98+
this will have implications on costs. Running this script once every 15-30 minutes should be totally fine.
99+
This means that there can be a "delay" in setting a project to finished about roughly the same time.
100+
"""
101+
try:
102+
filter_string = sys.argv[1]
103+
time_interval = int(sys.argv[2])
104+
except IndexError:
105+
logger.info("Please provide the filter_string and time_interval arguments.")
106+
sys.exit(1)
107+
108+
sched.every(time_interval).minutes.do(
109+
run_update_project_status, filter_string=filter_string
110+
).run()
111+
112+
while True:
113+
sched.run_pending()
114+
time.sleep(1)

0 commit comments

Comments
 (0)