Skip to content

Commit 97a6074

Browse files
authored
feat: Review's allocation generation | NPG-8066 (#552)
# Description - Added new cli command for allocations generation ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? - [x] test_allocations_generator ## Checklist - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
1 parent 8387460 commit 97a6074

File tree

12 files changed

+178
-74
lines changed

12 files changed

+178
-74
lines changed

src/event-db/stage_data/dev/00002_testfund_ideascale_params.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ INSERT INTO config (id, id2, id3, value) VALUES (
66
'{
77
"group_id": 37429,
88
"review_stage_ids": [171],
9-
"nr_allocations": [0, 0],
9+
"nr_allocations": [1, 1],
1010
"campaign_group_id": 88,
1111
"questions": {
1212
"Question 1": "Impact / Alignment",

utilities/ideascale-importer/ideascale_importer/cli/reviews.py

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
from typing import List
55
from loguru import logger
66

7-
from ideascale_importer.reviews_importer.importer import Importer
7+
from ideascale_importer.reviews_manager.manager import ReviewsManager
88
from ideascale_importer.utils import configure_logger
99

1010
app = typer.Typer(add_completion=False)
1111

12-
@app.command(name="import")
12+
@app.command(name="import-reviews")
1313
def import_reviews(
1414
ideascale_url: str = typer.Option(
1515
...,
@@ -59,24 +59,101 @@ def import_reviews(
5959
help="Log format",
6060
)
6161
):
62-
"""Import all reviews data from IdeaScale for a given funnel."""
62+
"""Import all reviews data from IdeaScale for the provided event."""
6363
configure_logger(log_level, log_format)
6464

6565
async def inner():
66-
importer = Importer(
66+
importer = ReviewsManager(
6767
ideascale_url=ideascale_url,
6868
database_url=database_url,
6969
email=email,
7070
password=password,
7171
api_token=api_token,
7272
event_id=event_id,
73-
allocations_path=allocations_path,
74-
output_path=output_path
7573
)
7674

7775
try:
7876
await importer.connect()
79-
await importer.run()
77+
await importer.import_reviews_run(
78+
allocations_path=allocations_path,
79+
output_path=output_path
80+
)
81+
await importer.close()
82+
except Exception as e:
83+
traceback.print_exc()
84+
logger.error(e)
85+
86+
asyncio.run(inner())
87+
88+
@app.command(name="prepare-allocations")
89+
def prepare_allocations(
90+
ideascale_url: str = typer.Option(
91+
...,
92+
envvar="IDEASCALE_API_URL",
93+
help="IdeaScale API URL",
94+
),
95+
database_url: str = typer.Option(
96+
...,
97+
envvar="EVENTDB_URL",
98+
help="Postgres database URL"
99+
),
100+
email: str = typer.Option(
101+
...,
102+
envvar="IDEASCALE_EMAIL",
103+
help="Ideascale user's email address (needs admin access)",
104+
),
105+
password: str = typer.Option(
106+
...,
107+
envvar="IDEASCALE_PASSWORD",
108+
help="Ideascale user's password (needs admin access)",
109+
),
110+
event_id: int = typer.Option(
111+
...,
112+
help="Database row id of the event which data will be imported",
113+
),
114+
api_token: str = typer.Option(
115+
...,
116+
envvar="IDEASCALE_API_TOKEN",
117+
help="IdeaScale API token"
118+
),
119+
pas_path: str = typer.Option(
120+
...,
121+
help="PAs file"
122+
),
123+
output_path: str = typer.Option(
124+
...,
125+
help="output path"
126+
),
127+
log_level: str = typer.Option(
128+
"info",
129+
envvar="REVIEWS_LOG_LEVEL",
130+
help="Log level",
131+
),
132+
log_format: str = typer.Option(
133+
"json",
134+
envvar="REVIEWS_LOG_FORMAT",
135+
help="Log format",
136+
)
137+
):
138+
"""Prepare allocations for the provided event."""
139+
configure_logger(log_level, log_format)
140+
141+
async def inner():
142+
importer = ReviewsManager(
143+
ideascale_url=ideascale_url,
144+
database_url=database_url,
145+
email=email,
146+
password=password,
147+
api_token=api_token,
148+
event_id=event_id,
149+
)
150+
151+
try:
152+
await importer.connect()
153+
await importer.generate_allocations_run(
154+
pas_path=pas_path,
155+
output_path=output_path
156+
)
80157
await importer.close()
81158
except Exception as e:
82159
traceback.print_exc()

utilities/ideascale-importer/ideascale_importer/reviews_importer/importer.py renamed to utilities/ideascale-importer/ideascale_importer/reviews_manager/manager.py

Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def login(self, email, password):
2929
'rememberMe': 'true',
3030
}
3131

32-
res = await self.inner.post(f"{login}", data=data)
32+
await self.inner.post(f"{login}", data=data)
3333

3434
async def download_reviews(self, reviews_path, review_stage_ids):
3535
async def download_file(self, review_stage_id):
@@ -61,7 +61,7 @@ async def download_file(self, review_stage_id):
6161
files.append(await download_file(self, review_stage_id))
6262
return files
6363

64-
class Importer:
64+
class ReviewsManager:
6565
def __init__(
6666
self,
6767
ideascale_url,
@@ -70,8 +70,6 @@ def __init__(
7070
password,
7171
event_id,
7272
api_token,
73-
allocations_path,
74-
output_path,
7573
):
7674
self.ideascale_url = ideascale_url
7775
self.database_url = database_url
@@ -80,15 +78,19 @@ def __init__(
8078
self.event_id = event_id
8179
self.api_token = api_token
8280

83-
self.allocations_path = allocations_path
84-
self.output_path = output_path
85-
86-
self.reviews_dir = tempfile.TemporaryDirectory()
87-
8881
self.frontend_client = None
8982
self.db = None
9083

91-
async def load_config(self):
84+
async def connect(self):
85+
if self.frontend_client is None:
86+
logger.info("Connecting to the Ideascale frontend")
87+
self.frontend_client = FrontendClient(self.ideascale_url)
88+
await self.frontend_client.login(self.email, self.password)
89+
if self.db is None:
90+
logger.info("Connecting to the database")
91+
self.db = await ideascale_importer.db.connect(self.database_url)
92+
93+
async def __load_config(self):
9294
"""Load the configuration setting from the event db."""
9395

9496
logger.info("Loading ideascale config from the event-db")
@@ -102,63 +104,61 @@ async def load_config(self):
102104
raise Exception("Cannot find ideascale config in the event-db database")
103105
self.config = Config(**res[0].value)
104106

105-
async def connect(self):
106-
if self.frontend_client is None:
107-
logger.info("Connecting to the Ideascale frontend")
108-
self.frontend_client = FrontendClient(self.ideascale_url)
109-
await self.frontend_client.login(self.email, self.password)
110-
if self.db is None:
111-
logger.info("Connecting to the database")
112-
self.db = await ideascale_importer.db.connect(self.database_url)
113-
114-
async def download_reviews(self):
115-
logger.info("Dowload reviews from Ideascale...")
107+
async def __download_reviews(self, reviews_output_path: str):
108+
logger.info("Download reviews from Ideascale...")
116109

117-
self.reviews = await self.frontend_client.download_reviews(self.reviews_dir.name, self.config.review_stage_ids)
110+
self.reviews = await self.frontend_client.download_reviews(reviews_output_path, self.config.review_stage_ids)
118111

119112
# This code will be moved as a separate cli command
120-
# async def prepare_allocations(self):
121-
# logger.info("Prepare allocations for proposal's reviews...")
122-
123-
# self.allocations_path = await allocate(
124-
# nr_allocations=self.config.nr_allocations,
125-
# pas_path=self.pa_path,
126-
# ideascale_api_key=self.api_token,
127-
# ideascale_api_url=self.ideascale_url,
128-
# stage_ids=self.config.stage_ids,
129-
# challenges_group_id=self.config.campaign_group_id,
130-
# group_id=self.config.group_id,
131-
# output_path=self.output_path,
132-
# )
113+
async def __prepare_allocations(self, pas_path: str, output_path: str):
114+
logger.info("Prepare allocations for proposal's reviews...")
115+
116+
self.allocations = await allocate(
117+
nr_allocations=self.config.nr_allocations,
118+
pas_path=pas_path,
119+
ideascale_api_key=self.api_token,
120+
ideascale_api_url=self.ideascale_url,
121+
stage_ids=self.config.stage_ids,
122+
challenges_group_id=self.config.campaign_group_id,
123+
group_id=self.config.group_id,
124+
output_path=output_path,
125+
)
133126

134-
async def prepare_reviews(self):
127+
async def __prepare_reviews(self, allocations_path: str, output_path: str):
135128
logger.info("Prepare proposal's reviews...")
136129
await process_ideascale_reviews(
137130
ideascale_xlsx_path=self.reviews,
138131
ideascale_api_url=self.ideascale_url,
139132
ideascale_api_key=self.api_token,
140-
allocation_path=self.allocations_path,
133+
allocation_path=allocations_path,
141134
challenges_group_id=self.config.campaign_group_id,
142135
questions=self.config.questions,
143136
fund=self.event_id,
144-
output_path=self.output_path
137+
output_path=output_path
145138
)
146139

147-
async def import_reviews(self):
148-
logger.info("Import reviews into Event db")
140+
async def __import_reviews_to_service(self):
141+
logger.info("Import reviews into cat data service")
142+
143+
async def generate_allocations_run(self, pas_path: str, output_path: str):
144+
"""Run the Allocations generation."""
145+
146+
await self.__load_config()
147+
await self.__prepare_allocations(pas_path=pas_path, output_path=output_path)
149148

150-
async def run(self):
151-
"""Run the importer."""
149+
async def import_reviews_run(self, allocations_path: str, output_path: str):
150+
"""Run the reviews importer."""
152151
if self.frontend_client is None:
153152
raise Exception("Not connected to the ideascale")
154153

155-
await self.load_config()
154+
await self.__load_config()
155+
reviews_dir = tempfile.TemporaryDirectory()
156+
await self.__download_reviews(reviews_output_path=reviews_dir.name)
157+
await self.__prepare_reviews(allocations_path=allocations_path, output_path=output_path)
156158

157-
await self.download_reviews()
158-
await self.prepare_reviews()
159+
reviews_dir.cleanup()
159160

160161
async def close(self):
161-
self.reviews_dir.cleanup()
162162
await self.frontend_client.close()
163163

164164
class Config(pydantic.BaseModel):

utilities/ideascale-importer/ideascale_importer/reviews_importer/processing/__init__.py renamed to utilities/ideascale-importer/ideascale_importer/reviews_manager/processing/__init__.py

File renamed without changes.

utilities/ideascale-importer/ideascale_importer/reviews_importer/processing/prepare.py renamed to utilities/ideascale-importer/ideascale_importer/reviews_manager/processing/prepare.py

File renamed without changes.

utilities/ideascale-importer/ideascale_importer/reviews_importer/processing/tools/allocator.py renamed to utilities/ideascale-importer/ideascale_importer/reviews_manager/processing/tools/allocator.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ def __init__(self, importer: Importer, nr_allocations: dict, seed: int, ideascal
2929
def __get_relevant_proposals(
3030
self, challenge_ids: List[int], source: List[models.Proposal], level: int, times_check: bool = True
3131
) -> List[models.Proposal]:
32+
if not challenge_ids:
33+
return []
34+
3235
times_picked = [len(p.allocations) for p in source]
3336
min_times_picked = min(times_picked)
3437
max_times_picked = max(times_picked)
3538
for x in range(min_times_picked, max_times_picked + 1):
36-
if len(challenge_ids) > 0:
37-
proposals = [p for p in source if (p.challenge_id in challenge_ids and (len(p.allocations) <= x and times_check))]
38-
else:
39-
proposals = [p for p in source if (len(p.allocations) <= x and times_check)]
39+
proposals = [p for p in source if (p.challenge_id in challenge_ids and (len(p.allocations) <= x and times_check))]
4040
if len(proposals) >= self.nr_allocations[level]:
4141
return proposals
4242
return proposals
@@ -48,17 +48,28 @@ def __generate_pa_challenges(self, pa: models.Pa, authors: dict) -> List[int]:
4848
If level 1, use preferences as base challenges.
4949
If level 0, use all challenges as base challenges.
5050
Filter out challenges as author.
51-
If no challenges remain and pa is author in some challenge, use all challenges as base and filter the one as author.
51+
If no challenges remain, use all challenges as base and filter the one as author.
5252
"""
5353
if pa.id in authors.keys():
5454
challenges_as_author = authors[pa.id]
5555
else:
5656
challenges_as_author = []
57-
base_challenges = pa.challenge_ids if pa.level == 1 else []
58-
challenges_ids = list(filter(lambda c: c not in challenges_as_author, base_challenges))
59-
if len(challenges_ids) == 0 and len(challenges_as_author) > 0:
60-
challenges_ids = list(filter(lambda c: c not in challenges_as_author, [c.id for c in self.source.challenges]))
61-
return challenges_ids
57+
58+
base_challenges_ids = [c.id for c in self.source.challenges]
59+
# If level 0, use all challenges as base challenges.
60+
if pa.level == 0:
61+
# Filter out challenges as author.
62+
return list(filter(lambda c: c not in challenges_as_author, base_challenges_ids))
63+
# If level 1, use preferences as base challenges.
64+
if pa.level == 1:
65+
# Filter out challenges as author.
66+
challenges_ids = list(filter(lambda c: c not in challenges_as_author, pa.challenge_ids))
67+
# If no challenges remain, use all challenges as base and filter the one as author.
68+
if len(challenges_ids) == 0:
69+
return list(filter(lambda c: c not in challenges_as_author, base_challenges_ids))
70+
return challenges_ids
71+
72+
raise Exception(f"Invalid pa level: {pa.level}")
6273

6374
def __generate_authors_map(self):
6475
logger.info("Generating authors map...")

utilities/ideascale-importer/ideascale_importer/reviews_importer/processing/tools/importer.py renamed to utilities/ideascale-importer/ideascale_importer/reviews_manager/processing/tools/importer.py

File renamed without changes.

utilities/ideascale-importer/ideascale_importer/reviews_importer/processing/tools/postprocessor.py renamed to utilities/ideascale-importer/ideascale_importer/reviews_manager/processing/tools/postprocessor.py

File renamed without changes.

utilities/ideascale-importer/ideascale_importer/reviews_importer/processing/types/models.py renamed to utilities/ideascale-importer/ideascale_importer/reviews_manager/processing/types/models.py

File renamed without changes.

utilities/ideascale-importer/ideascale_importer/reviews_importer/processing/utils.py renamed to utilities/ideascale-importer/ideascale_importer/reviews_manager/processing/utils.py

File renamed without changes.

0 commit comments

Comments
 (0)