From 4308cffdfe082cff09875e539bc085a132a19ca9 Mon Sep 17 00:00:00 2001 From: Jeroen Date: Sun, 26 Nov 2023 08:14:34 +0100 Subject: [PATCH 1/2] Added Google location logic --- .gitignore | 1 + public/port-0.0.0-py3-none-any.whl | Bin 5360 -> 6326 bytes .../py/dist/port-0.0.0-py3-none-any.whl | Bin 5360 -> 6326 bytes src/framework/processing/py/port/script.py | 296 ++++++++++++------ .../processing/py/tests/script_test.py | 156 +++++++++ .../react/ui/pages/donation_page.tsx | 6 +- 6 files changed, 359 insertions(+), 100 deletions(-) create mode 100644 src/framework/processing/py/tests/script_test.py diff --git a/.gitignore b/.gitignore index 8b69396..eabcd78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +__pycache__/ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. release.zip diff --git a/public/port-0.0.0-py3-none-any.whl b/public/port-0.0.0-py3-none-any.whl index 0e25e81a9afcfaa5f60b483d10cef00ec04cdabc..2032c42111a1a45c0f5ea3849714e3739cd5052f 100644 GIT binary patch delta 3682 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 delta 2728 zcmZ8j2T&6V5)B|tLLeYT2vP*T5QKnqHNdAy(U3?0DIci?jXo+tD3gUEA;5r){_*c1qNXpvxr(S%`>L#(f zZi$$*sXPPva1F%8XXcos44ADQuy`F!ioALQN<-a7dFzr@Yw%4Z`6iRH3$bLBBnz*2 zZywsi7y11vy4vy2&~-LzdN+ zyk`wt7EoZkT$ezOl4(WsLm3Yw7U_5cm^0C``TUASqs1+UG-qMWZf(+{+>B^a_+8Bc zb-uzi@Nu4#JN-TjPB3&JxF-aC{)-yK3w#|(s*&2n+Z|RUA*8Q*N{#Nhy9HS}sEl^- zM&WvcLRtp-C=ZzkR-@h-!zBUQsgyItMkIagfjbvkV*Uf1jO#Tm0 zaS|znV|t_SoGdo(l6f@C^^Q^!M)OH>I(jdadZIE}!BWCgnp)`I{TU>6GXRq1;y0b= zHi5@+Of8ufsg39Hy68$g{3%h<5T*`-g4&pSGoE5B89$bu3$>?f8U8AKdmOE2>EzfK zB@}IQtJ^G7g&N~7bgMGEWrz5eECVVoGFteGu(rJyQM4QSTsESWs;izdX@6nH^8I3_5SInLP3i+e=%)j|6J& zdSvHX5za&>Nq$^Z^aNj86PElQ?z~Wd)rodxUdTa(m#+`qYrP%0meSre?b4>cVZ2^6 zf;H^hwfC2nTXB}KIt4Tntvod|;wyH|7t;#D@iR9N? z^lsJ8%9QCC@?+5bc}JgjW?Ha4`V=!6HyM*(DG>8Pq>jMvEi?i?aQ)ogW_cZp@usqA zblJT?-ju<6za^PE3jtg4y|6;7cgxRALT$T32(TFMU`rdqD-j1TZFWJt$0<(b*V6O| zXETzbK7q&NyxF%+bqw#^gCXRNpz_)JuQdsfGh>T%XqwsuZjV_8@M1nEv~+zhuw>B8 zEzP!gMWi6kfQoGUdTIFa;yeCR7i{mhDVe{p4N%B(@du@T%v#L=DkLai$Usf*ERYeE zR4nHg20_f2$(saR;PS_hj;P^Xrx#c|T?VNP5HY*c5v^H;$YLA}|KLh%CU|Ky%`%l; zpU}XB#%b*Rb`OXF#xkPy8IDstO~mq6>9h2u2YEar`3$}N7ImCyCghs%onA#TNpnno zDyYFI{Ip{B968=IfZvSpeNu4RK-|zg_S`i)?Jx2#vu6#7=J*0Rc{Q0}xpLsyJ$3<4 z+bzM5xwT;aQ31Y)3oOj|RhV60IHvcL=HAd`t6i zNhEz!cKw7olg{Th=e>`q4R%>Tu>ZH$W7mULImI35jT3hc;$m&oFyy} z2v@MtmiAgGqT>hJ>IRTY)x`%S+CJZE_uUzxuYx*_6j9paOVu4YVJeaDRj(Ge+F#k;)lbw1=1efb8Gx6a2 z^1(962IZufiSsgn6q~Wj{paq*Ra-6GqjB)fKa)aJP7YIu)Yau}WQKoKcMifjvLS1Z z(|CHx?Lof))8@+JR1`CC%e9{MV^1eP<-l0jJw9RQSv6vCexg@#f$w+7zE9EV=p)Du zM=J3PvKZd}lBuaRf2Yy$7#Ts%zWTCFc45<$cYG#wiRJW`vST?(NU=%y$VP#h;Bri< z%-UMtjyIR_ZPBn@VNYsk3aNi6Sh%A~-Z4ckzH6+4)0>It3X>P*il8lU|5o}^ySpyD z?5NY>>;Qn|QTbor#95eX#-11mk1{nzpiBb#x^N^z0CDE_W6pFl)h0QcXXjw>khhmOSVJ_x@;AZ}> zYIO+iZ9Z&howq81iu&oY{?y0dJ)P|q!??eYP0l*esE%QG?lH}()kG1z)Osir(MT56 z!OWT@b!6A=<37Z=Dm91k=jFpK9xT%=I1?60fa^*RWfv?NgMq$6*LKj}h~?sbXmv@N zhLFvjvjgtiKq=xOvLh{pBlPKs|!D1KjgBYO72YxELiQEN3VrqX&P~xho^JtiJ^$(E{w|*%b2E_ z@P5%{Y`Ts52cZYJR=RAxbtQHzdZIe^Ln1q~pec^|d_FN;#qao!m)d`#?TvTL>7O0 diff --git a/src/framework/processing/py/dist/port-0.0.0-py3-none-any.whl b/src/framework/processing/py/dist/port-0.0.0-py3-none-any.whl index 0e25e81a9afcfaa5f60b483d10cef00ec04cdabc..2032c42111a1a45c0f5ea3849714e3739cd5052f 100644 GIT binary patch delta 3682 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 delta 2728 zcmZ8j2T&6V5)B|tLLeYT2vP*T5QKnqHNdAy(U3?0DIci?jXo+tD3gUEA;5r){_*c1qNXpvxr(S%`>L#(f zZi$$*sXPPva1F%8XXcos44ADQuy`F!ioALQN<-a7dFzr@Yw%4Z`6iRH3$bLBBnz*2 zZywsi7y11vy4vy2&~-LzdN+ zyk`wt7EoZkT$ezOl4(WsLm3Yw7U_5cm^0C``TUASqs1+UG-qMWZf(+{+>B^a_+8Bc zb-uzi@Nu4#JN-TjPB3&JxF-aC{)-yK3w#|(s*&2n+Z|RUA*8Q*N{#Nhy9HS}sEl^- zM&WvcLRtp-C=ZzkR-@h-!zBUQsgyItMkIagfjbvkV*Uf1jO#Tm0 zaS|znV|t_SoGdo(l6f@C^^Q^!M)OH>I(jdadZIE}!BWCgnp)`I{TU>6GXRq1;y0b= zHi5@+Of8ufsg39Hy68$g{3%h<5T*`-g4&pSGoE5B89$bu3$>?f8U8AKdmOE2>EzfK zB@}IQtJ^G7g&N~7bgMGEWrz5eECVVoGFteGu(rJyQM4QSTsESWs;izdX@6nH^8I3_5SInLP3i+e=%)j|6J& zdSvHX5za&>Nq$^Z^aNj86PElQ?z~Wd)rodxUdTa(m#+`qYrP%0meSre?b4>cVZ2^6 zf;H^hwfC2nTXB}KIt4Tntvod|;wyH|7t;#D@iR9N? z^lsJ8%9QCC@?+5bc}JgjW?Ha4`V=!6HyM*(DG>8Pq>jMvEi?i?aQ)ogW_cZp@usqA zblJT?-ju<6za^PE3jtg4y|6;7cgxRALT$T32(TFMU`rdqD-j1TZFWJt$0<(b*V6O| zXETzbK7q&NyxF%+bqw#^gCXRNpz_)JuQdsfGh>T%XqwsuZjV_8@M1nEv~+zhuw>B8 zEzP!gMWi6kfQoGUdTIFa;yeCR7i{mhDVe{p4N%B(@du@T%v#L=DkLai$Usf*ERYeE zR4nHg20_f2$(saR;PS_hj;P^Xrx#c|T?VNP5HY*c5v^H;$YLA}|KLh%CU|Ky%`%l; zpU}XB#%b*Rb`OXF#xkPy8IDstO~mq6>9h2u2YEar`3$}N7ImCyCghs%onA#TNpnno zDyYFI{Ip{B968=IfZvSpeNu4RK-|zg_S`i)?Jx2#vu6#7=J*0Rc{Q0}xpLsyJ$3<4 z+bzM5xwT;aQ31Y)3oOj|RhV60IHvcL=HAd`t6i zNhEz!cKw7olg{Th=e>`q4R%>Tu>ZH$W7mULImI35jT3hc;$m&oFyy} z2v@MtmiAgGqT>hJ>IRTY)x`%S+CJZE_uUzxuYx*_6j9paOVu4YVJeaDRj(Ge+F#k;)lbw1=1efb8Gx6a2 z^1(962IZufiSsgn6q~Wj{paq*Ra-6GqjB)fKa)aJP7YIu)Yau}WQKoKcMifjvLS1Z z(|CHx?Lof))8@+JR1`CC%e9{MV^1eP<-l0jJw9RQSv6vCexg@#f$w+7zE9EV=p)Du zM=J3PvKZd}lBuaRf2Yy$7#Ts%zWTCFc45<$cYG#wiRJW`vST?(NU=%y$VP#h;Bri< z%-UMtjyIR_ZPBn@VNYsk3aNi6Sh%A~-Z4ckzH6+4)0>It3X>P*il8lU|5o}^ySpyD z?5NY>>;Qn|QTbor#95eX#-11mk1{nzpiBb#x^N^z0CDE_W6pFl)h0QcXXjw>khhmOSVJ_x@;AZ}> zYIO+iZ9Z&howq81iu&oY{?y0dJ)P|q!??eYP0l*esE%QG?lH}()kG1z)Osir(MT56 z!OWT@b!6A=<37Z=Dm91k=jFpK9xT%=I1?60fa^*RWfv?NgMq$6*LKj}h~?sbXmv@N zhLFvjvjgtiKq=xOvLh{pBlPKs|!D1KjgBYO72YxELiQEN3VrqX&P~xho^JtiJ^$(E{w|*%b2E_ z@P5%{Y`Ts52cZYJR=RAxbtQHzdZIe^Ln1q~pec^|d_FN;#qao!m)d`#?TvTL>7O0 diff --git a/src/framework/processing/py/port/script.py b/src/framework/processing/py/port/script.py index 7abb7d7..22ee8b7 100644 --- a/src/framework/processing/py/port/script.py +++ b/src/framework/processing/py/port/script.py @@ -1,62 +1,154 @@ +import fnmatch +import json +from datetime import datetime +from collections import namedtuple + import port.api.props as props -from port.api.commands import (CommandSystemDonate, CommandUIRender) +from port.api.commands import CommandSystemDonate, CommandUIRender import pandas as pd import zipfile +ExtractionResult = namedtuple("ExtractionResult", ["id", "title", "data_frame"]) + + +def get_in(dct, *key_path): + for key in key_path: + dct = dct.get(key) + if dct is None: + return + return dct + + +def parse_json_to_dataframe(parsed_dict): + data = [] + for obj in parsed_dict["timelineObjects"]: + if "activitySegment" not in obj: + continue + + segment = obj["activitySegment"] + activity_type = segment["activityType"] + + if activity_type not in {"WALKING", "CYCLING","RUNNING"}: + continue + + start_timestamp_str = segment["duration"]["startTimestamp"] + start_timestamp = datetime.fromisoformat( + start_timestamp_str[:-1] + ) # remove the 'Z' + + if meters := get_in(segment, "waypointPath", "distanceMeters"): + distance_meters = meters + elif meters := get_in(segment, "simplifiedRawPath", "distanceMeters"): + distance_meters = meters + elif meters := segment.get("distance"): + distance_meters = meters + else: + continue + + data.append([start_timestamp, activity_type, distance_meters]) + + return pd.DataFrame( + data, columns=["startTimestamp", "activityType", "distanceMeters"] + ) + + +def aggregate_distance_by_day_activity(df): + # Format the startTimestamp to "year-month-day" format + df["startTimestamp"] = df["startTimestamp"].dt.strftime("%Y-%m-%d") + + # Group by formatted date and activityType, then aggregate the distance + aggregated_df = ( + df.groupby(["startTimestamp", "activityType"])["distanceMeters"] + .sum() + .reset_index() + ) + + return aggregated_df + + +def extract(df): + aggregated_df = aggregate_distance_by_day_activity(df) + aggregated_df["Afstand in km"] = aggregated_df["distanceMeters"] / 1000 + + results = [] + for activity_type, title in [ + ("WALKING", {"en": "Walking", "nl": "Gewandeld"}), + ("CYCLING", {"en": "Cycling", "nl": "Gefietst"}), + ("RUNNING", {"en": "Running", "nl": "Hardgelopen"}), + ]: + df = aggregated_df.loc[aggregated_df["activityType"] == activity_type] + if len(df) == 0: + continue + + df["Datum"] = df["startTimestamp"] + df = ( + df.drop(columns=["distanceMeters", "activityType", "startTimestamp"]) + .reset_index(drop=True) + .reindex(columns=["Datum", "Afstand in km"]) + ) + results.append( + ExtractionResult( + id=activity_type.lower(), + title=props.Translatable(title), + data_frame=df, + ) + ) + return results + def process(sessionId): yield donate(f"{sessionId}-tracking", '[{ "message": "user entered script" }]') - platforms = ["Twitter", "Facebook", "Instagram", "Youtube"] - - subflows = len(platforms) - steps = 2 - step_percentage = (100/subflows)/steps - - # progress in % - progress = 0 - - for index, platform in enumerate(platforms): - meta_data = [] - meta_data.append(("debug", f"{platform}: start")) - - # STEP 1: select the file - progress += step_percentage - data = None - while True: - meta_data.append(("debug", f"{platform}: prompt file")) - promptFile = prompt_file(platform, "application/zip, text/plain") - fileResult = yield render_donation_page(platform, promptFile, progress) - if fileResult.__type__ == 'PayloadString': - meta_data.append(("debug", f"{platform}: extracting file")) - extractionResult = doSomethingWithTheFile(platform, fileResult.value) - if extractionResult != 'invalid': - meta_data.append(("debug", f"{platform}: extraction successful, go to consent form")) - data = extractionResult + meta_data = [] + meta_data.append(("debug", f"start")) + + # STEP 1: select the file + data = None + while True: + print("A") + promptFile = prompt_file() + print("B") + fileResult = yield render_donation_page(promptFile, 33) + print("C") + if fileResult.__type__ == "PayloadString": + meta_data.append(("debug", f"extracting file")) + extractionResult = extract_data_from_zip(fileResult.value) + if extractionResult == "invalid": + meta_data.append( + ("debug", f"prompt confirmation to retry file selection") + ) + retry_result = yield render_donation_page(retry_confirmation(), 33) + if retry_result.__type__ == "PayloadTrue": + meta_data.append(("debug", f"skip due to invalid file")) + continue + else: + meta_data.append(("debug", f"retry prompt file")) break + if extractionResult == 'no-data': + retry_result = yield render_donation_page(retry_no_data_confirmation(), 33) + if retry_result.__type__ == "PayloadTrue": + continue else: - meta_data.append(("debug", f"{platform}: prompt confirmation to retry file selection")) - retry_result = yield render_donation_page(platform, retry_confirmation(platform), progress) - if retry_result.__type__ == 'PayloadTrue': - meta_data.append(("debug", f"{platform}: skip due to invalid file")) - continue - else: - meta_data.append(("debug", f"{platform}: retry prompt file")) - break + break else: - meta_data.append(("debug", f"{platform}: skip to next step")) + meta_data.append( + ("debug", f"extraction successful, go to consent form") + ) + data = extractionResult break - - # STEP 2: ask for consent - progress += step_percentage - if data is not None: - meta_data.append(("debug", f"{platform}: prompt consent")) - prompt = prompt_consent(platform, data, meta_data) - consent_result = yield render_donation_page(platform, prompt, progress) - if consent_result.__type__ == "PayloadJSON": - meta_data.append(("debug", f"{platform}: donate consent data")) - yield donate(f"{sessionId}-{platform}", consent_result.value) + else: + meta_data.append(("debug", f"skip to next step")) + break + + # STEP 2: ask for consent + if data is not None: + meta_data.append(("debug", f"prompt consent")) + prompt = prompt_consent(data, meta_data) + consent_result = yield render_donation_page(prompt, 67) + if consent_result.__type__ == "PayloadJSON": + meta_data.append(("debug", f"donate consent data")) + yield donate(f"{sessionId}", consent_result.value) yield render_end_page() @@ -66,78 +158,92 @@ def render_end_page(): return CommandUIRender(page) -def render_donation_page(platform, body, progress): +def render_donation_page(body, progress): header = props.PropsUIHeader(props.Translatable({ - "en": platform, - "nl": platform + "en": "Google activity", + "nl": "Google activity" })) footer = props.PropsUIFooter(progress) - page = props.PropsUIPageDonation(platform, header, body, footer) + page = props.PropsUIPageDonation("google-activity", header, body, footer) return CommandUIRender(page) -def retry_confirmation(platform): - text = props.Translatable({ - "en": f"Unfortunately, we cannot process your {platform} file. Continue, if you are sure that you selected the right file. Try again to select a different file.", - "nl": f"Helaas, kunnen we uw {platform} bestand niet verwerken. Weet u zeker dat u het juiste bestand heeft gekozen? Ga dan verder. Probeer opnieuw als u een ander bestand wilt kiezen." - }) - ok = props.Translatable({ - "en": "Try again", - "nl": "Probeer opnieuw" - }) - cancel = props.Translatable({ - "en": "Continue", - "nl": "Verder" - }) +def retry_confirmation(): + text = props.Translatable( + { + "en": f"Unfortunately, we cannot process your file. Continue, if you are sure that you selected the right file. Try again to select a different file.", + "nl": f"Helaas, kunnen we uw bestand niet verwerken. Weet u zeker dat u het juiste bestand heeft gekozen? Ga dan verder. Probeer opnieuw als u een ander bestand wilt kiezen.", + } + ) + ok = props.Translatable({"en": "Try again", "nl": "Probeer opnieuw"}) + cancel = props.Translatable({"en": "Continue", "nl": "Verder"}) + return props.PropsUIPromptConfirm(text, ok, cancel) + +def retry_no_data_confirmation(): + text = props.Translatable( + { + "en": f"There does not seem to be location information in your file. Continue, if you are sure that you selected the right file. Try again to select a different file.", + "nl": f"Helaas, er lijkt geen lokatie informatie in uw bestand te zitten. Weet u zeker dat u het juiste bestand heeft gekozen? Ga dan verder. Probeer opnieuw als u een ander bestand wilt kiezen.", + } + ) + ok = props.Translatable({"en": "Try again", "nl": "Probeer opnieuw"}) + cancel = props.Translatable({"en": "Continue", "nl": "Verder"}) return props.PropsUIPromptConfirm(text, ok, cancel) -def prompt_file(platform, extensions): + +def prompt_file(): description = props.Translatable({ - "en": f"Please follow the download instructions and choose the file that you stored on your device. Click “Skip” at the right bottom, if you do not have a {platform} file. ", - "nl": f"Volg de download instructies en kies het bestand dat u opgeslagen heeft op uw apparaat. Als u geen {platform} bestand heeft klik dan op “Overslaan” rechts onder." + "en": f"Please follow the download instructions and choose the file that you stored on your device. Click 'Skip' at the right bottom, if you do not have a file. ", + "nl": f"Volg de download instructies en kies het bestand dat u opgeslagen heeft op uw apparaat. Als u geen bestand heeft klik dan op 'Overslaan' rechts onder.", }) - return props.PropsUIPromptFileInput(description, extensions) + return props.PropsUIPromptFileInput(description, "application/zip") -def doSomethingWithTheFile(platform, filename): - return extract_zip_contents(filename) +def prompt_consent(tables, meta_data): + log_title = props.Translatable({"en": "Log messages", "nl": "Log berichten"}) + tables = [ + props.PropsUIPromptConsentFormTable(table.id, table.title, table.data_frame) + for table in tables + ] + meta_frame = pd.DataFrame(meta_data, columns=["type", "message"]) + meta_table = props.PropsUIPromptConsentFormTable( + "log_messages", log_title, meta_frame + ) + return props.PropsUIPromptConsentForm(tables, [meta_table]) -def extract_zip_contents(filename): - names = [] - try: - file = zipfile.ZipFile(filename) - data = [] - for name in file.namelist(): - names.append(name) - info = file.getinfo(name) - data.append((name, info.compress_size, info.file_size)) - return data - except zipfile.error: - return "invalid" +def filter_json_files(file_list): + pattern = "**/Semantic Location History/*/*_*.json" + return [f for f in file_list if fnmatch.fnmatch(f, pattern)] -def prompt_consent(id, data, meta_data): - table_title = props.Translatable({ - "en": "Zip file contents", - "nl": "Inhoud zip bestand" - }) +def load_and_process_file(z, file, callback): + with z.open(file) as f: + return callback(json.load(f)) - log_title = props.Translatable({ - "en": "Log messages", - "nl": "Log berichten" - }) - data_frame = pd.DataFrame(data, columns=["filename", "compressed size", "size"]) - table = props.PropsUIPromptConsentFormTable("zip_content", table_title, data_frame) - meta_frame = pd.DataFrame(meta_data, columns=["type", "message"]) - meta_table = props.PropsUIPromptConsentFormTable("log_messages", log_title, meta_frame) - return props.PropsUIPromptConsentForm([table], [meta_table]) +def extract_data_from_zip(zip_filepath): + with zipfile.ZipFile(zip_filepath, "r") as z: + files = filter_json_files(z.namelist()) + dfs = [load_and_process_file(z, f, parse_json_to_dataframe) for f in files] + if not dfs: + return "no-data" + df = pd.concat(dfs, ignore_index=True) + return extract(df) def donate(key, json_string): return CommandSystemDonate(key, json_string) + + +if __name__ == "__main__": + import sys + + if len(sys.argv) > 1: + print(extract_data_from_zip(sys.argv[1])) + else: + print("please provide a zip file as argument") diff --git a/src/framework/processing/py/tests/script_test.py b/src/framework/processing/py/tests/script_test.py new file mode 100644 index 0000000..650d846 --- /dev/null +++ b/src/framework/processing/py/tests/script_test.py @@ -0,0 +1,156 @@ +from datetime import datetime +import pytest +import zipfile + + +from port.script import parse_json_to_dataframe +from port.script import aggregate_distance_by_day_activity +from port.script import extract +from port.script import extract_data_from_zip + + +@pytest.fixture +def sample_data(): + return { + "timelineObjects": [ + { + "activitySegment": { + "duration": {"startTimestamp": "2023-04-01T19:13:27.023Z"}, + "activityType": "CYCLING", + "waypointPath": {"distanceMeters": 3600.33}, + } + } + ] + } + + +@pytest.fixture +def sample_data_multiple_activities(): + return { + "timelineObjects": [ + { + "activitySegment": { + "duration": {"startTimestamp": "2023-04-01T19:13:27.023Z"}, + "activityType": "CYCLING", + "waypointPath": {"distanceMeters": 3600.33}, + } + }, + { + "activitySegment": { + "duration": {"startTimestamp": "2023-04-01T20:13:27.023Z"}, + "activityType": "CYCLING", + "waypointPath": {"distanceMeters": 1400.0}, + } + }, + { + "activitySegment": { + "duration": {"startTimestamp": "2023-04-02T08:13:27.023Z"}, + "activityType": "WALKING", + "waypointPath": {"distanceMeters": 800.5}, + } + }, + { + "activitySegment": { + "duration": {"startTimestamp": "2023-04-01T19:13:27.023Z"}, + "activityType": "RUNNING", + "waypointPath": {"distanceMeters": 3600.33}, + } + }, + { + "activitySegment": { + "duration": {"startTimestamp": "2023-04-01T20:13:27.023Z"}, + "activityType": "RUNNING", + "waypointPath": {"distanceMeters": 1400.0}, + } + }, + ] + } + + +def test_parse_json_to_dataframe(sample_data): + df = parse_json_to_dataframe(sample_data) + assert len(df) == 1 + assert df.iloc[0]["activityType"] == "CYCLING" + assert df.iloc[0]["distanceMeters"] == 3600.33 + assert isinstance(df.iloc[0]["startTimestamp"], datetime) + + +def test_parse_json_to_dataframe_skips_non_walking_or_cycling(): + parsed_dict = { + "timelineObjects": [ + { + "activitySegment": { + "activityType": "WALKING", + "duration": {"startTimestamp": "2023-09-17T10:00:00Z"}, + "waypointPath": {"distanceMeters": 1000}, + } + }, + { + "activitySegment": { + "activityType": "CYCLING", + "duration": {"startTimestamp": "2023-09-17T11:00:00Z"}, + "waypointPath": {"distanceMeters": 5000}, + } + }, + { + "activitySegment": { + "activityType": "DRIVING", + "duration": {"startTimestamp": "2023-09-17T12:00:00Z"}, + "waypointPath": {"distanceMeters": 20000}, + } + }, + ] + } + + df = parse_json_to_dataframe(parsed_dict) + assert "DRIVING" not in df.activityType.values + + +def test_aggregate_distance_by_day_activity(sample_data): + df = parse_json_to_dataframe(sample_data) + aggregated_df = aggregate_distance_by_day_activity(df) + + assert len(aggregated_df) == 1 + assert aggregated_df.iloc[0]["startTimestamp"] == "2023-04-01" + assert aggregated_df.iloc[0]["activityType"] == "CYCLING" + assert aggregated_df.iloc[0]["distanceMeters"] == 3600.33 + + +def test_aggregation_over_multiple_activities(sample_data_multiple_activities): + df = parse_json_to_dataframe(sample_data_multiple_activities) + aggregated_df = aggregate_distance_by_day_activity(df) + + # Verify that there are 2 aggregated entries (one for each day) + assert len(aggregated_df) == 3 + + # For 2023-04-01, there were two cycling activities. We sum their distances. + cycling_data = aggregated_df[(aggregated_df["activityType"] == "CYCLING")] + assert len(cycling_data) == 1 + assert cycling_data.iloc[0]["distanceMeters"] == (3600.33 + 1400.0) + + # For 2023-04-02, there was one walking activity. + walking_data = aggregated_df[aggregated_df["activityType"] == "WALKING"] + assert len(walking_data) == 1 + assert walking_data.iloc[0]["distanceMeters"] == 800.5 + + # For 2023-05-02, there was one running activity. + walking_data = aggregated_df[aggregated_df["activityType"] == "RUNNING"] + assert len(walking_data) == 1 + assert walking_data.iloc[0]["distanceMeters"] == (3600.33 + 1400.0) + + +def test_extract_sample_data(sample_data): + results = extract(parse_json_to_dataframe(sample_data)) + # Verify the results + assert len(results) == 1 + assert results[0].id == "cycling" + assert results[0].title.translations["nl"] == "Gefietst" + for result in results: + assert "distanceMeters" not in result.data_frame.columns + assert "Afstand in km" in result.data_frame.columns + +def test_empty_zip(tmp_path): + path = tmp_path.joinpath("test.zip") + z = zipfile.ZipFile(path, "w") + z.close() + assert extract_data_from_zip(path) == "no-data" \ No newline at end of file diff --git a/src/framework/visualisation/react/ui/pages/donation_page.tsx b/src/framework/visualisation/react/ui/pages/donation_page.tsx index f4f45cd..e2a81a8 100644 --- a/src/framework/visualisation/react/ui/pages/donation_page.tsx +++ b/src/framework/visualisation/react/ui/pages/donation_page.tsx @@ -17,13 +17,12 @@ import { Sidebar } from './templates/sidebar' import LogoSvg from '../../../../../assets/images/logo.svg' import { Page } from './templates/page' import { Progress } from '../elements/progress' -import { Instructions } from '../elements/instructions' type Props = Weak & ReactFactoryContext export const DonationPage = (props: Props): JSX.Element => { const { title, forwardButton } = prepareCopy(props) - const { platform, locale, resolve } = props + const { locale, resolve } = props function renderBody (props: Props): JSX.Element { const context = { locale: locale, resolve: props.resolve } @@ -62,9 +61,6 @@ export const DonationPage = (props: Props): JSX.Element => { const sidebar: JSX.Element = ( - } /> ) From 635bb29fde8086d1b5b762652468c420bd93f116 Mon Sep 17 00:00:00 2001 From: Adrienne Mendrik <79082794+AdrienneMendrik@users.noreply.github.com> Date: Sat, 2 Dec 2023 15:54:01 +0100 Subject: [PATCH 2/2] Updated texts in script.py --- src/framework/processing/py/port/script.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/framework/processing/py/port/script.py b/src/framework/processing/py/port/script.py index 22ee8b7..1a64b61 100644 --- a/src/framework/processing/py/port/script.py +++ b/src/framework/processing/py/port/script.py @@ -160,8 +160,8 @@ def render_end_page(): def render_donation_page(body, progress): header = props.PropsUIHeader(props.Translatable({ - "en": "Google activity", - "nl": "Google activity" + "en": "Google location", + "nl": "Google locatie" })) footer = props.PropsUIFooter(progress) @@ -172,8 +172,8 @@ def render_donation_page(body, progress): def retry_confirmation(): text = props.Translatable( { - "en": f"Unfortunately, we cannot process your file. Continue, if you are sure that you selected the right file. Try again to select a different file.", - "nl": f"Helaas, kunnen we uw bestand niet verwerken. Weet u zeker dat u het juiste bestand heeft gekozen? Ga dan verder. Probeer opnieuw als u een ander bestand wilt kiezen.", + "en": f"Unfortunately we cannot process your file. Continue, if you are sure that you selected the right file. Try again to select a different file.", + "nl": f"Helaas kunnen we uw bestand niet verwerken. Weet u zeker dat u het juiste bestand heeft gekozen? Ga dan verder. Probeer opnieuw als u een ander bestand wilt kiezen.", } ) ok = props.Translatable({"en": "Try again", "nl": "Probeer opnieuw"}) @@ -183,8 +183,8 @@ def retry_confirmation(): def retry_no_data_confirmation(): text = props.Translatable( { - "en": f"There does not seem to be location information in your file. Continue, if you are sure that you selected the right file. Try again to select a different file.", - "nl": f"Helaas, er lijkt geen lokatie informatie in uw bestand te zitten. Weet u zeker dat u het juiste bestand heeft gekozen? Ga dan verder. Probeer opnieuw als u een ander bestand wilt kiezen.", + "en": f"Unfortunately we could not detect any location information in your file. Continue, if you are sure that you selected the right file. Try again to select a different file.", + "nl": f"We hebben helaas geen locatie informatie in uw bestand gevonden. Weet u zeker dat u het juiste bestand heeft gekozen? Ga dan verder. Probeer opnieuw als u een ander bestand wilt kiezen.", } ) ok = props.Translatable({"en": "Try again", "nl": "Probeer opnieuw"}) @@ -195,8 +195,8 @@ def retry_no_data_confirmation(): def prompt_file(): description = props.Translatable({ - "en": f"Please follow the download instructions and choose the file that you stored on your device. Click 'Skip' at the right bottom, if you do not have a file. ", - "nl": f"Volg de download instructies en kies het bestand dat u opgeslagen heeft op uw apparaat. Als u geen bestand heeft klik dan op 'Overslaan' rechts onder.", + "en": f"Click 'Choose file' to choose the file that you received from Google. If you click 'Continue', the data that is required for research is extracted from your file.", + "nl": f"Klik op ‘Kies bestand’ om het bestand dat u ontvangen hebt van Google te kiezen. Als u op 'Verder' klikt worden de gegevens die nodig zijn voor het onderzoek uit uw bestand gehaald.", }) return props.PropsUIPromptFileInput(description, "application/zip")