Skip to content

Commit a8ce6ad

Browse files
authored
Merge pull request #21 from jvaleroliet/new_features
added new functionalities
2 parents 22fe52a + 8693e11 commit a8ce6ad

File tree

2 files changed

+191
-7
lines changed

2 files changed

+191
-7
lines changed

pykobo/form.py

+48-7
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ def fetch_data(self) -> Union[pd.DataFrame, dict]:
6969

7070
# If the form has at least one repeat group
7171
if self.has_repeats:
72-
7372
self._extract_repeats(data)
7473

7574
# In the parent DF delete the columns that contain the repeat groups
@@ -162,6 +161,27 @@ def display(self, columns_as: str = "name", choices_as: str = "name") -> None:
162161
)
163162
self.__choices_as = choices_as
164163

164+
def fetch_media(self):
165+
"""Fetch the form's media files and store them as a Pandas DF in the attribute `media`."""
166+
# Create media url
167+
media_url = f"{self.base_url}/{self.uid}/files/?format=json"
168+
# Request media and extract dataframe
169+
res = requests.get(url=media_url, headers=self.headers)
170+
media = res.json()["results"]
171+
if not media.empty:
172+
media[["hash", "filename", "mimetype"]] = pd.json_normalize(media.metadata)
173+
self.media = pd.DataFrame(media)
174+
175+
def fetch_attachments(self, media_columns: list):
176+
"""
177+
This function returns attached media in new columns,
178+
one for each question with media, given df."""
179+
180+
for column in media_columns:
181+
self.data["media_" + column] = self.data.apply(
182+
lambda x: self._obtain_url(x, column), axis=1
183+
)
184+
165185
def _get_survey(self) -> None:
166186
"""Go through all the elements of the survey and build the root structure (and the structure
167187
of the repeat groups if any) as a list of `Question` objects. Each `Question` object has a name
@@ -182,16 +202,13 @@ def _get_survey(self) -> None:
182202
in_repeat = False
183203

184204
for field in survey:
185-
186205
# Identify groups and repeats if any
187206
if field["type"] == "begin_group":
188-
189207
group_name = field["name"]
190208
if "label" in field:
191209
group_label = field["label"]
192210

193211
if field["type"] == "begin_repeat":
194-
195212
repeat_name = field["name"]
196213
if "label" in field:
197214
repeat_label = field["label"]
@@ -218,7 +235,6 @@ def _get_survey(self) -> None:
218235
and field["type"] != "end_group"
219236
and field["type"] != "end_repeat"
220237
):
221-
222238
name_q = field["$autoname"]
223239
if "label" in field:
224240
label_q = field["label"][0]
@@ -272,7 +288,8 @@ def _get_survey(self) -> None:
272288

273289
def _get_choices(self):
274290
"""For all the questions of type 'select_one' or 'select_multiple' assign their corresponding choices.
275-
Each choice has a name and label so it's possible to display the data using any of the two."""
291+
Each choice has a name and label so it's possible to display the data using any of the two.
292+
"""
276293

277294
formatted_choices = {}
278295
choices = self.__content["choices"]
@@ -398,7 +415,6 @@ def _remove_unused_columns(self) -> None:
398415
"formhub/uuid",
399416
"meta/instanceID",
400417
"_xform_id_string",
401-
"_attachments",
402418
"meta/deprecatedID",
403419
"_geolocation",
404420
]
@@ -425,6 +441,31 @@ def _rename_columns_labels_duplicates(self, structure: list) -> None:
425441
duplicates_count[q.label] += 1
426442
q.label = f"{q.label} ({duplicates_count[q.label]})"
427443

444+
def _obtain_url(self, row, column):
445+
"""Aux Function to obtain url of an attached file.
446+
Replaces the ' ' (spaces) by '_' from the attached files"""
447+
448+
df = pd.json_normalize(row["_attachments"])
449+
if "filename" in df.columns:
450+
df["filename_ok"] = df["filename"].apply(
451+
lambda x: x.split("/")[-1].replace(" ", "_")
452+
)
453+
454+
if pd.isna(row[column]):
455+
name = None
456+
else:
457+
name = row[column].replace(" ", "_")
458+
459+
if name is not None:
460+
matching_rows = df.loc[df["filename_ok"] == name]
461+
if not matching_rows.empty:
462+
url = matching_rows["download_url"].iloc[0]
463+
else:
464+
url = np.nan
465+
else:
466+
url = np.nan
467+
return url
468+
428469
def _split_gps_coords(self) -> None:
429470
"""Split the columns of type 'geopoint' into 4 new columns
430471
'latitude', 'longitude', 'altitude', 'gps_precision'

pykobo/manager.py

+143
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import json
2+
import logging
3+
import os
4+
import time
15
from typing import Union
26

37
import requests
@@ -79,3 +83,142 @@ def get_form(self, uid: str) -> Union[KoboForm, None]:
7983
kform = self._create_koboform(form)
8084

8185
return kform
86+
87+
def redeploy_form(self, uid: str) -> None:
88+
url = f"{self.url}/api/v{self.api_version}/assets/{uid}/deployment/?format=json"
89+
requests.patch(url=url, headers=self.headers)
90+
91+
def upload_media_from_local(
92+
self, uid: str, folder_path: str, file_name: str, rewrite: bool = False
93+
) -> None:
94+
file_extension = os.path.splitext(file_name)[1]
95+
valid_media = [".jpeg", ".jpg", ".png", ".csv", ".JPGE", ".JPG", ".PNG"]
96+
97+
if not folder_path.endswith(("/", "\\")):
98+
folder_path += "/"
99+
100+
if file_extension not in valid_media:
101+
raise ValueError(
102+
"upload_media_from_local: file extension must be one of %r."
103+
% valid_media
104+
)
105+
106+
file_path = os.path.join(folder_path, file_name)
107+
108+
if not os.path.exists(file_path):
109+
raise FileNotFoundError(f"File not found: {file_path}")
110+
111+
self._upload_media(uid, open(file_path, "rb"), file_name, rewrite)
112+
113+
def upload_media_from_server(
114+
self, uid: str, media_data: bytes, file_name: str, rewrite: bool = False
115+
) -> None:
116+
self._upload_media(uid, media_data, file_name, rewrite)
117+
118+
def _upload_media(
119+
self, uid: str, media_data: bytes, file_name: str, rewrite: bool
120+
) -> None:
121+
url_media = f"{self.url}/api/v{self.api_version}/assets/{uid}/files"
122+
payload = {"filename": file_name}
123+
data = {
124+
"description": "default",
125+
"metadata": json.dumps(payload),
126+
"data_value": file_name,
127+
"file_type": "form_media",
128+
}
129+
130+
res = requests.get(f"{url_media}.json", headers=self.headers)
131+
res.raise_for_status()
132+
dict_response = res.json()["results"]
133+
134+
for each in dict_response:
135+
if each["metadata"]["filename"] == file_name:
136+
if rewrite:
137+
del_id = each["uid"]
138+
res.status_code = 403
139+
while res.status_code != 204:
140+
res = requests.delete(
141+
f"{url_media}/{del_id}", headers=self.headers
142+
)
143+
time.sleep(1)
144+
break
145+
else:
146+
raise ValueError(
147+
"There is already a file with the same name! Select a new name or set 'rewrite=True'"
148+
)
149+
150+
files = {"content": (file_name, media_data)} # Pass media_data directly
151+
152+
res = requests.post(
153+
url=f"{url_media}.json", data=data, files=files, headers=self.headers
154+
)
155+
res.raise_for_status()
156+
157+
if res.status_code == 201:
158+
logging.info(f"Successfully uploaded {file_name} to {uid} form.")
159+
else:
160+
logging.error(f"Unsuccessful. Response code: {str(res.status_code)}")
161+
162+
def share_project(self, uid: str, user: str, permission: str):
163+
"""
164+
Share a project with a user.
165+
166+
Parameters
167+
----------
168+
uid : str
169+
The project's uid.
170+
user : str
171+
The user's uid.
172+
permission : str
173+
The permission to give the user.
174+
"""
175+
176+
valid_permissions = [
177+
"add_submissions",
178+
"change_asset",
179+
"change_submissions",
180+
"delete_submissions",
181+
"discover_asset",
182+
"manage_asset",
183+
"partial_submissions",
184+
"validate_submissions",
185+
"view_asset",
186+
"view_submissions",
187+
]
188+
189+
if permission not in valid_permissions:
190+
raise ValueError(
191+
"Permission must be one of the following: " + str(valid_permissions)
192+
)
193+
194+
data = {
195+
"user": f"{self.url}/api/v{self.api_version}/users/{user}/",
196+
"permission": f"{self.url}/api/v{self.api_version}/permissions/{permission}/",
197+
}
198+
199+
url = f"{self.url}/api/v{self.api_version}/assets/{uid}/permission-assignments.json"
200+
res = requests.post(url=url, headers=self.headers, data=data)
201+
202+
if res.status_code != 201:
203+
raise requests.HTTPError(res.text)
204+
205+
def fetch_users_with_access(self, uid: str):
206+
"""
207+
Fetch the list of users who have access to a specific form, extracting usernames from URLs.
208+
"""
209+
url_permissions = f"{self.url}/api/v{self.api_version}/assets/{uid}/permission-assignments/"
210+
res = requests.get(url=url_permissions, headers=self.headers)
211+
212+
if res.status_code != 200:
213+
raise requests.HTTPError(f"Failed to fetch permissions: {res.text}")
214+
215+
permissions = res.json()
216+
users_with_access = set()
217+
218+
for permission in permissions:
219+
user_url = permission.get('user')
220+
if user_url:
221+
username = user_url.rstrip('/').split('/')[-1] # Split to get the username from the url
222+
users_with_access.add(username)
223+
224+
return list(users_with_access)

0 commit comments

Comments
 (0)