46 |
80 |
81 |
82 |
83 | {% if user.is_authenticated %}
84 | {% block content %}
85 | {% endblock %}
86 | {% else %}
87 |
88 |
89 |
Please Login to view the page
90 |
91 |
92 | {% endif %}
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | {% block post_scripts %}
107 | {% endblock %}
108 |
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/home/views/web/intentionalwalk.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import itertools
3 |
4 | from django.db.models import Sum
5 | from django.views import generic
6 |
7 | from home.models import Account, DailyWalk, IntentionalWalk
8 | from home.templatetags.format_helpers import m_to_mi
9 |
10 | # Date range for data aggregation
11 | DEFAULT_START_DATE = datetime.date(2020, 4, 1)
12 | DEFAULT_END_DATE = datetime.datetime.today().date()
13 |
14 |
15 | class IntentionalWalkWebView(generic.ListView):
16 | template_name = "home/iw_list.html"
17 | model = IntentionalWalk
18 |
19 | # Augment context data to
20 | def get_context_data(self, **kwargs):
21 | # Call the base implementation first to get a context
22 | context = super().get_context_data(**kwargs)
23 |
24 | start_date_str = self.request.GET.get("start_date")
25 | start_date = (
26 | datetime.datetime.strptime(start_date_str, "%Y-%m-%d").date()
27 | if start_date_str
28 | else DEFAULT_START_DATE
29 | )
30 | end_date_str = self.request.GET.get("end_date")
31 | end_date = (
32 | datetime.datetime.strptime(end_date_str, "%Y-%m-%d").date()
33 | if end_date_str
34 | else DEFAULT_END_DATE
35 | )
36 |
37 | # Get aggregate stats for all users
38 | all_accounts = Account.objects.all().order_by("created")
39 |
40 | # Get recorded walks per day
41 | recorded_walks = IntentionalWalk.objects.all().values()
42 | recorded_walks_stats = {}
43 | for date, group in itertools.groupby(
44 | recorded_walks, key=lambda x: x["start"].date()
45 | ):
46 | recorded_walks_stats[date] = {
47 | "count": 0,
48 | "steps": 0,
49 | "time": 0,
50 | "miles": 0,
51 | }
52 | for obj in group:
53 | recorded_walks_stats[date]["count"] += 1 # Update count
54 | recorded_walks_stats[date]["steps"] += obj[
55 | "steps"
56 | ] # Update count
57 | recorded_walks_stats[date]["time"] += (
58 | obj["end"] - obj["start"]
59 | ).total_seconds() - obj["pause_time"]
60 | recorded_walks_stats[date]["miles"] += m_to_mi(
61 | obj["distance"]
62 | ) # Update count
63 |
64 | # Fill the gaps cos google charts if annoying af
65 | current_date = start_date
66 | delta = datetime.timedelta(days=1)
67 | context["daily_recorded_walks_stats"] = []
68 | # Iterate over the entire date range
69 | while current_date <= end_date:
70 | context["daily_recorded_walks_stats"].append(
71 | [
72 | current_date,
73 | recorded_walks_stats.get(
74 | current_date,
75 | {"count": 0, "steps": 0, "time": 0, "miles": 0},
76 | ),
77 | ]
78 | )
79 | current_date += delta
80 | context["cumu_recorded_walks_stats"] = []
81 | total = {"count": 0, "steps": 0, "time": 0, "miles": 0}
82 | for date, stat_obj in context["daily_recorded_walks_stats"]:
83 | # NOTE: Counters represent 0 counts as an empty dict and wont
84 | # guarantee keys existence
85 | total["count"] += stat_obj["count"]
86 | total["steps"] += stat_obj["steps"]
87 | total["time"] += stat_obj["time"]
88 | total["miles"] += stat_obj["miles"]
89 | context["cumu_recorded_walks_stats"].append([date, dict(total)])
90 | context["total_iw_stats"] = total
91 | context["total_iw_stats"]["time"] = int(
92 | context["total_iw_stats"]["time"] / 3600
93 | )
94 |
95 | # Get IW users
96 | context["total_iw_users"] = (
97 | IntentionalWalk.objects.values("account").distinct().count()
98 | )
99 | context["total_signedup"] = len(all_accounts)
100 | context["percent_usage"] = (
101 | (context["total_iw_users"] / context["total_signedup"]) * 100
102 | if context["total_signedup"] > 0
103 | else 0
104 | )
105 | context["total_steps"] = DailyWalk.objects.all().aggregate(
106 | Sum("steps")
107 | )["steps__sum"]
108 | context["percent_steps"] = (
109 | (context["total_iw_stats"]["steps"] / context["total_steps"]) * 100
110 | if context["total_steps"] > 0
111 | else 0
112 | )
113 | context["total_distance"] = m_to_mi(
114 | DailyWalk.objects.all().aggregate(Sum("distance"))["distance__sum"]
115 | )
116 | context["percent_distance"] = (
117 | (context["total_iw_stats"]["miles"] / context["total_distance"])
118 | * 100
119 | if context["total_distance"] > 0
120 | else 0
121 | )
122 |
123 | context["start_date"] = start_date
124 | context["end_date"] = end_date
125 | context["min_date"] = DEFAULT_START_DATE
126 | context["max_date"] = DEFAULT_END_DATE
127 |
128 | return context
129 |
--------------------------------------------------------------------------------
/static/home/home_plots.js:
--------------------------------------------------------------------------------
1 | // TODO: This objectively horrible, hacky, spaghetti code that must be refactored
2 | // Function to plot daily and cumulative user growths
3 | google.charts.load('current', {'packages':['bar', 'line', 'corechart']});
4 | google.charts.setOnLoadCallback(plot);
5 | function plot() {
6 |
7 | // --------------------------------- 1. Bar plot - All Signups ---------------------------------
8 | var data = new google.visualization.DataTable();
9 | data.addColumn('date', 'Date');
10 | data.addColumn('number', 'Signups');
11 | data.addRows(daily_user_signups)
12 | var options = {
13 | height: 400,
14 | legend: { position: 'none' },
15 | bar: { groupWidth: '95%' },
16 | vAxis: {
17 | title: "Daily signups",
18 | viewWindow: { min: 0 }
19 | },
20 | colors: ["#E59866"]
21 | };
22 | var chart = new google.charts.Bar(document.getElementById('daily_signups'));
23 | chart.draw(data, google.charts.Bar.convertOptions(options));
24 |
25 | // --------------------------------- 2. Line Plot - All Signups ---------------------------------
26 | var data = new google.visualization.DataTable();
27 | data.addColumn('date', 'Date');
28 | data.addColumn('number', 'Signups');
29 | data.addRows(cumu_user_signups)
30 | var options = {
31 | legend: { position: 'none' },
32 | height: 400,
33 | vAxis: {
34 | title: "Total signups",
35 | viewWindow: { min: 0 }
36 | },
37 | colors: ["#E59866"]
38 | };
39 | var chart = new google.charts.Line(document.getElementById('total_signups'));
40 | chart.draw(data, google.charts.Line.convertOptions(options));
41 |
42 | // --------------------------------- 3. Bar Plot - All Steps ---------------------------------
43 | var data = new google.visualization.DataTable();
44 | data.addColumn('date', 'Date');
45 | data.addColumn('number', 'Steps');
46 | data.addRows(daily_step_count)
47 | var options = {
48 | height: 400,
49 | legend: { position: 'none' },
50 | bar: { groupWidth: '95%' },
51 | vAxis: {
52 | title: "Steps",
53 | viewWindow: { min: 0 }
54 | },
55 | colors: ["#2ECC71"]
56 | };
57 | var chart = new google.charts.Bar(document.getElementById('daily_steps'));
58 | chart.draw(data, google.charts.Bar.convertOptions(options));
59 |
60 | // --------------------------------- 4. Line Plot - All Steps ---------------------------------
61 | var data = new google.visualization.DataTable();
62 | data.addColumn('date', 'Date');
63 | data.addColumn('number', 'Steps');
64 | data.addRows(cumu_step_count)
65 | var options = {
66 | legend: { position: 'none' },
67 | height: 400,
68 | vAxis: {
69 | title: "Steps",
70 | viewWindow: { min: 0 }
71 | },
72 | colors: ["#2ECC71"]
73 | };
74 | var chart = new google.charts.Line(document.getElementById('total_steps'));
75 | chart.draw(data, google.charts.Line.convertOptions(options));
76 |
77 | // --------------------------------- 5. Bar Plot - All Miles ---------------------------------
78 | var data = new google.visualization.DataTable();
79 | data.addColumn('date', 'Date');
80 | data.addColumn('number', 'Miles');
81 | data.addRows(daily_mile_count)
82 | var options = {
83 | height: 400,
84 | legend: { position: 'none' },
85 | bar: { groupWidth: '95%' },
86 | vAxis: {
87 | title: "Miles",
88 | viewWindow: { min: 0 }
89 | },
90 | colors: ["#1ABC9C"]
91 | };
92 | var chart = new google.charts.Bar(document.getElementById('daily_miles'));
93 | chart.draw(data, google.charts.Bar.convertOptions(options));
94 |
95 | // --------------------------------- 6. Line Plot - All Miles ---------------------------------
96 | var data = new google.visualization.DataTable();
97 | data.addColumn('date', 'Date');
98 | data.addColumn('number', 'Miles');
99 | data.addRows(cumu_mile_count)
100 | var options = {
101 | legend: { position: 'none' },
102 | height: 400,
103 | vAxis: {
104 | title: "Miles",
105 | viewWindow: { min: 0 }
106 | },
107 | colors: ["#1ABC9C"]
108 | };
109 | var chart = new google.charts.Line(document.getElementById('total_miles'));
110 | chart.draw(data, google.charts.Line.convertOptions(options));
111 |
112 | // --------------------------------- 7. Age histogram ---------------------------------
113 | var data = new google.visualization.DataTable();
114 | data.addColumn('number', 'Age');
115 | data.addRows(user_age_dist)
116 | var options = {
117 | legend: { position: 'none' },
118 | height: 600,
119 | colors: ["#AF7AC5"],
120 | histogram: { bucketSize: 5 },
121 | vAxis: {
122 | title: "Number of users",
123 | viewWindow: { min: 0 }
124 | },
125 | hAxis: {
126 | title: "Age",
127 | },
128 | };
129 |
130 | var chart = new google.visualization.Histogram(document.getElementById('user_age_dist'));
131 | chart.draw(data, options);
132 |
133 | };
--------------------------------------------------------------------------------
/home/tests/integration/weeklygoal/test_get.py:
--------------------------------------------------------------------------------
1 | from django.test import Client, TestCase
2 | from django.forms.models import model_to_dict
3 |
4 | from home.models import WeeklyGoal, Device
5 |
6 |
7 | class ApiTestCase(TestCase):
8 | def setUp(self):
9 | # Test client
10 | self.client = Client()
11 |
12 | self.email = "abhay@blah.com"
13 | self.account_id = "12345"
14 |
15 | # Create a user
16 | response = self.client.post(
17 | path="/api/appuser/create",
18 | data={
19 | "name": "Abhay Kashyap",
20 | "email": self.email,
21 | "zip": "72185",
22 | "age": 99,
23 | "account_id": self.account_id,
24 | },
25 | content_type="application/json",
26 | )
27 |
28 | # Check for a successful response by the server
29 | self.assertEqual(response.status_code, 200)
30 | # Parse the response
31 | response_data = response.json()
32 |
33 | fail_message = f"Server response - {response_data}"
34 | self.assertEqual(response_data["status"], "success", msg=fail_message)
35 | self.assertEqual(
36 | response_data["message"],
37 | "Device registered & account registered successfully",
38 | msg=fail_message,
39 | )
40 | device = Device.objects.get(device_id=self.account_id)
41 | self.account = device.account
42 |
43 | # Define weekly goals
44 | self.weekly_goals = [
45 | WeeklyGoal(
46 | account=self.account,
47 | start_of_week="2023-08-21",
48 | steps=3,
49 | days=2000,
50 | ),
51 | WeeklyGoal(
52 | account=self.account,
53 | start_of_week="2023-08-27",
54 | steps=4,
55 | days=2500,
56 | ),
57 | WeeklyGoal(
58 | account=self.account,
59 | start_of_week="2023-09-04",
60 | steps=5,
61 | days=3000,
62 | ),
63 | ]
64 |
65 | # Create weekly goals
66 | response = WeeklyGoal.objects.bulk_create(self.weekly_goals)
67 |
68 | i = 0
69 | # Check for a successful response by the server
70 | for item in response:
71 | self.assertEqual(item, self.weekly_goals[i])
72 | i += 1
73 |
74 | # Details for intentional walk list view
75 | self.url = "/api/weeklygoal/get"
76 | # Request parameters
77 | # Content type
78 | self.content_type = "application/json"
79 | self.request_params = {"account_id": self.account_id}
80 |
81 | # Test a successful request for weekly goals
82 | def test_weeklygoal_get_success(self):
83 | # Send the request
84 | response = self.client.post(
85 | path=self.url,
86 | data=self.request_params,
87 | content_type=self.content_type,
88 | )
89 | # Check for a successful response by the server
90 | self.assertEqual(response.status_code, 200)
91 | # Parse the response
92 | response_data = response.json()
93 | fail_message = f"Server response - {response_data}"
94 | self.assertEqual(response_data["status"], "success", msg=fail_message)
95 | self.assertIn("payload", response_data, msg=fail_message)
96 | i = 2
97 | for goal in response_data["payload"]:
98 | goalDict = model_to_dict(self.weekly_goals[i])
99 | goalDict["account_id"] = goalDict.get("account")
100 | del goalDict["account"]
101 | self.assertEqual(goal, goalDict)
102 | i -= 1
103 |
104 | # Test getting weekly goals from an account that doesn't exist
105 | def test_weeklygoal_get_failure_invalid_account(self):
106 | self.request_params["account_id"] = "0000000"
107 |
108 | # Send the request
109 | response = self.client.post(
110 | path=self.url,
111 | data=self.request_params,
112 | content_type=self.content_type,
113 | )
114 | # Check for a successful response by the server
115 | self.assertEqual(response.status_code, 200)
116 | # Parse the response
117 | response_data = response.json()
118 |
119 | fail_message = f"Server response - {response_data}"
120 | self.assertEqual(response_data["status"], "error", msg=fail_message)
121 | self.assertEqual(
122 | response_data["message"],
123 | "Unregistered account - "
124 | f'{self.request_params["account_id"]}.'
125 | " Please register first!",
126 | msg=fail_message,
127 | )
128 |
129 | # Test getting weekly goals with missing account id param
130 | def test_weeklygoal_get_failure_missing_account_id(self):
131 | del self.request_params["account_id"]
132 |
133 | # Send the request
134 | response = self.client.post(
135 | path=self.url,
136 | data=self.request_params,
137 | content_type=self.content_type,
138 | )
139 | # Check for a successful response by the server
140 | self.assertEqual(response.status_code, 200)
141 | # Parse the response
142 | response_data = response.json()
143 |
144 | fail_message = f"Server response - {response_data}"
145 | self.assertEqual(response_data["status"], "error", msg=fail_message)
146 | self.assertEqual(
147 | response_data["message"],
148 | "Required input 'account_id' missing in the request",
149 | msg=fail_message,
150 | )
151 |
--------------------------------------------------------------------------------
/home/views/api/serializers/request_serializers.py:
--------------------------------------------------------------------------------
1 | """
2 | This module contains serializers that are used for parsing and validating request data.
3 |
4 | Each serializer in this module corresponds to a specific API endpoint. The serializer's
5 | `validate` method is responsible for validating the incoming request data and preparing
6 | it for further processing.
7 | """
8 |
9 | from rest_framework import serializers
10 | from datetime import timedelta
11 | from home.models import Contest
12 | from django.db.models import (
13 | BooleanField,
14 | Count,
15 | ExpressionWrapper,
16 | F,
17 | Q,
18 | Sum,
19 | )
20 |
21 |
22 | class GetUsersReqSerializer(serializers.Serializer):
23 | contest_id = serializers.CharField(
24 | required=False,
25 | help_text="The ID of the contest to filter by."
26 | + "Providing this also will add additional metrics related to te contest.",
27 | )
28 | # If true, will only return tester accounts.
29 | is_tester = serializers.BooleanField(
30 | required=False, help_text="If true, will only return tester accounts."
31 | )
32 | # Choices are: age, contests, created, dailywalk, device, email, gender, gender_other, id,
33 | # intentionalwalk, is_latino, is_sf_resident, is_tester, iw_count, iw_distance, iw_steps,
34 | # iw_time, leaderboard, name, race, race_other, sexual_orien, sexual_orien_other, updated,
35 | # weeklygoal, zip.
36 | # TODO: Can move this to the choices field tuple.
37 | # which will allow some tools to auto-pick up.
38 | order_by = serializers.CharField(
39 | required=False,
40 | help_text="The field to order the results by. Prefix with '-' to order in descending order."
41 | + "The secondary sort and default sort will be lexicographically, the 'name'.",
42 | )
43 | page = serializers.IntegerField(
44 | required=False,
45 | help_text="The page number to return. Defaults to 1.",
46 | default=1,
47 | )
48 | query = serializers.CharField(
49 | required=False,
50 | help_text="Query string to filter for containment in the name or email.",
51 | )
52 |
53 | def validate(self, data):
54 | """Validates and prepares the incoming request data.
55 |
56 | Converts the request params into FilterSet params and annotations.
57 | """
58 | contest_id = data.get("contest_id")
59 | is_tester = data.get("is_tester")
60 | order_by = data.get("order_by")
61 | page = data.get("page") or 1
62 | per_page = 25
63 | query = data.get("query")
64 |
65 | # filter and annotate based on contest_id
66 | filters, annotate, intentionalwalk_filter = None, None, None
67 | if contest_id:
68 | contest = Contest.objects.get(pk=contest_id)
69 | dailywalk_filter = Q(
70 | dailywalk__date__range=(contest.start, contest.end)
71 | )
72 |
73 | filters = Q(contests__contest_id=contest_id)
74 | annotate = {
75 | "is_new": ExpressionWrapper(
76 | Q(
77 | created__gte=contest.start_promo,
78 | created__lt=contest.end + timedelta(days=1),
79 | ),
80 | output_field=BooleanField(),
81 | ),
82 | "dw_count": Count("dailywalk", filter=dailywalk_filter),
83 | "dw_steps": Sum("dailywalk__steps", filter=dailywalk_filter),
84 | "dw_distance": Sum(
85 | "dailywalk__distance", filter=dailywalk_filter
86 | ),
87 | }
88 | intentionalwalk_filter = Q(
89 | intentionalwalk__start__gte=contest.start,
90 | intentionalwalk__start__lt=contest.end + timedelta(days=1),
91 | )
92 | else:
93 | filters = Q()
94 | annotate = {
95 | "dw_count": Count("dailywalk"),
96 | "dw_steps": Sum("dailywalk__steps"),
97 | "dw_distance": Sum("dailywalk__distance"),
98 | }
99 | intentionalwalk_filter = Q()
100 |
101 | intentionalwalk_annotate = {
102 | "iw_count": Count(
103 | "intentionalwalk", filter=intentionalwalk_filter
104 | ),
105 | "iw_steps": Sum(
106 | "intentionalwalk__steps", filter=intentionalwalk_filter
107 | ),
108 | "iw_distance": Sum(
109 | "intentionalwalk__distance", filter=intentionalwalk_filter
110 | ),
111 | "iw_time": Sum(
112 | "intentionalwalk__walk_time", filter=intentionalwalk_filter
113 | ),
114 | }
115 |
116 | # filter to show users vs testers
117 | filters &= Q(is_tester=is_tester)
118 |
119 | # filter by search query
120 | if query:
121 | filters &= Q(Q(name__icontains=query) | Q(email__icontains=query))
122 |
123 | # set ordering
124 | order = []
125 | if order_by:
126 | desc = order_by.startswith("-")
127 | field = F(order_by[1:] if desc else order_by)
128 | order.append(
129 | field.desc(nulls_last=True)
130 | if desc
131 | else field.asc(nulls_first=None)
132 | )
133 | order.append(F("name"))
134 |
135 | return {
136 | "annotate": annotate,
137 | "intentionalwalk_annotate": intentionalwalk_annotate,
138 | "contest_id": contest_id,
139 | "filters": filters,
140 | "order_by": order,
141 | "page": page,
142 | "per_page": per_page,
143 | }
144 |
--------------------------------------------------------------------------------
/home/urls.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.urls import path
4 | from django.views.generic import TemplateView
5 |
6 | from . import views
7 |
8 | PRODUCTION = os.getenv("DEPLOY_ENV") == "production"
9 |
10 | app_name = "home"
11 |
12 | urlpatterns = []
13 | if PRODUCTION:
14 | # serve the React SPA index.html as a catch-all
15 | urlpatterns = [
16 | path("users/", TemplateView.as_view(template_name="index.html")),
17 | path("login/", TemplateView.as_view(template_name="index.html")),
18 | ]
19 | else:
20 | # mount old views for comparison testing until fully deprecated and removed
21 | urlpatterns = [
22 | path("", views.HomeView.as_view(), name="home_view"),
23 | path("users/", views.UserListView.as_view(), name="user_list_view"),
24 | path(
25 | "intentionalwalks/",
26 | views.IntentionalWalkWebView.as_view(),
27 | name="int_walk_list_view",
28 | ),
29 | path(
30 | "data/users_agg.csv",
31 | views.user_agg_csv_view,
32 | name="users_agg_csv_view",
33 | ),
34 | path("data/users.csv", views.users_csv_view, name="users_csv_view"),
35 | path(
36 | "data/daily_walks.csv",
37 | views.daily_walks_csv_view,
38 | name="dailywalks_csv_view",
39 | ),
40 | path(
41 | "data/intentional_walks.csv",
42 | views.intentional_walks_csv_view,
43 | name="intentionalwalks_csv_view",
44 | ),
45 | ]
46 |
47 | urlpatterns.extend(
48 | [
49 | path("api/admin/me", views.AdminMeView.as_view(), name="admin_me"),
50 | path(
51 | "api/admin/home", views.AdminHomeView.as_view(), name="admin_home"
52 | ),
53 | path(
54 | "api/admin/home/users/daily",
55 | views.AdminHomeUsersDailyView.as_view(),
56 | name="admin_home_users_daily",
57 | ),
58 | path(
59 | "api/admin/home/users/cumulative",
60 | views.AdminHomeUsersCumulativeView.as_view(),
61 | name="admin_home_users_cumulative",
62 | ),
63 | path(
64 | "api/admin/home/steps/daily",
65 | views.AdminHomeStepsDailyView.as_view(),
66 | name="admin_home_steps_daily",
67 | ),
68 | path(
69 | "api/admin/home/steps/cumulative",
70 | views.AdminHomeStepsCumulativeView.as_view(),
71 | name="admin_home_steps_cumulative",
72 | ),
73 | path(
74 | "api/admin/home/distance/daily",
75 | views.AdminHomeDistanceDailyView.as_view(),
76 | name="admin_home_distance_daily",
77 | ),
78 | path(
79 | "api/admin/home/distance/cumulative",
80 | views.AdminHomeDistanceCumulativeView.as_view(),
81 | name="admin_home_distance_cumulative",
82 | ),
83 | path(
84 | "api/admin/contests",
85 | views.AdminContestsView.as_view(),
86 | name="admin_contests",
87 | ),
88 | path(
89 | "api/admin/users",
90 | views.AdminUsersView.as_view(),
91 | name="admin_users",
92 | ),
93 | path(
94 | "api/admin/users/zip",
95 | views.AdminUsersByZipView.as_view(),
96 | name="admin_users_zip",
97 | ),
98 | path(
99 | "api/admin/users/zip/active",
100 | views.AdminUsersActiveByZipView.as_view(),
101 | name="admin_users_zip_active",
102 | ),
103 | path(
104 | "api/admin/users/zip/steps",
105 | views.AdminUsersByZipMedianStepsView.as_view(),
106 | name="admin_users_zip_steps",
107 | ),
108 | path(
109 | "api/admin/
/histogram",
110 | views.AdminHistogramView.as_view(),
111 | name="export_users",
112 | ),
113 | path(
114 | "api/appuser/create",
115 | views.AppUserCreateView.as_view(),
116 | name="appuser_create",
117 | ),
118 | path(
119 | "api/appuser/delete",
120 | views.AppUserDeleteView.as_view(),
121 | name="appuser_delete",
122 | ),
123 | path(
124 | "api/dailywalk/create",
125 | views.DailyWalkCreateView.as_view(),
126 | name="dailywalk_create",
127 | ),
128 | path(
129 | "api/dailywalk/get",
130 | views.DailyWalkListView.as_view(),
131 | name="dailywalk_get",
132 | ),
133 | path(
134 | "api/export/users",
135 | views.ExportUsersView.as_view(),
136 | name="export_users",
137 | ),
138 | path(
139 | "api/intentionalwalk/create",
140 | views.IntentionalWalkView.as_view(),
141 | name="intentionalwalk_create",
142 | ),
143 | path(
144 | "api/intentionalwalk/get",
145 | views.IntentionalWalkListView.as_view(),
146 | name="intentionalwalk_get",
147 | ),
148 | path(
149 | "api/contest/current",
150 | views.ContestCurrentView.as_view(),
151 | name="contest_current",
152 | ),
153 | path(
154 | "api/leaderboard/get/",
155 | views.LeaderboardListView.as_view(),
156 | name="leaderboard_get",
157 | ),
158 | path(
159 | "api/weeklygoal/create",
160 | views.WeeklyGoalCreateView.as_view(),
161 | name="weeklygoal_create",
162 | ),
163 | path(
164 | "api/weeklygoal/get",
165 | views.WeeklyGoalsListView.as_view(),
166 | name="weeklygoal_get",
167 | ),
168 | ]
169 | )
170 |
--------------------------------------------------------------------------------
/server/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for server project.
3 |
4 | Generated by 'django-admin startproject' using Django 3.0.3.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.0/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/3.0/ref/settings/
11 | """
12 |
13 | import os
14 | from pathlib import Path
15 |
16 | import dj_database_url
17 | from dotenv import find_dotenv, load_dotenv
18 |
19 | import sentry_sdk
20 |
21 | load_dotenv(find_dotenv())
22 |
23 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
24 | BASE_DIR = Path(__file__).resolve().parent.parent
25 | PROJECT_ROOT = BASE_DIR
26 |
27 | # Initialize Sentry SDK if DSN is set
28 | SENTRY_DSN = os.getenv("SENTRY_DSN")
29 | if SENTRY_DSN:
30 | sentry_sdk.init(
31 | # SECURITY WARNING: keep the Sentry DSN secret!
32 | dsn=SENTRY_DSN,
33 | # Set traces_sample_rate to 1.0 to capture 100%
34 | # of transactions for tracing.
35 | traces_sample_rate=1.0,
36 | # Set profiles_sample_rate to 1.0 to profile 100%
37 | # of sampled transactions.
38 | # We recommend adjusting this value in production.
39 | profiles_sample_rate=1.0,
40 | )
41 |
42 | # Quick-start development settings - unsuitable for production
43 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
44 |
45 | # SECURITY WARNING: keep the secret key used in production secret!
46 | SECRET_KEY = os.getenv("SECRET_KEY")
47 |
48 | # SECURITY WARNING: don't run with debug turned on in production!
49 | PRODUCTION = os.getenv("DEPLOY_ENV") == "production"
50 | DEBUG = bool(os.getenv("DEBUG", not PRODUCTION))
51 |
52 | # Static files configuration
53 | STATIC_URL = "static/"
54 | STATICFILES_DIRS = [PROJECT_ROOT / "static"]
55 | STATIC_ROOT = PROJECT_ROOT / "staticfiles"
56 |
57 | if PRODUCTION:
58 | WHITENOISE_INDEX_FILE = True
59 | WHITENOISE_ROOT = BASE_DIR / "client" / "dist"
60 | STATICFILES_STORAGE = (
61 | "whitenoise.storage.CompressedManifestStaticFilesStorage"
62 | )
63 |
64 | # Host configuration
65 | ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost").split(",")
66 |
67 | CSRF_TRUSTED_ORIGINS = os.getenv(
68 | "CSRF_TRUSTED_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000"
69 | ).split(",")
70 |
71 | # Application definition
72 |
73 | INSTALLED_APPS = [
74 | "django.contrib.admin",
75 | "django.contrib.auth",
76 | "django.contrib.contenttypes",
77 | "django.contrib.sessions",
78 | "django.contrib.messages",
79 | "django.contrib.admindocs",
80 | "django.contrib.humanize",
81 | "home.apps.HomeConfig",
82 | ]
83 |
84 | if PRODUCTION:
85 | INSTALLED_APPS.append("whitenoise.runserver_nostatic")
86 | INSTALLED_APPS.append("django.contrib.staticfiles")
87 |
88 | MIDDLEWARE = [
89 | "django.middleware.security.SecurityMiddleware",
90 | "django.contrib.sessions.middleware.SessionMiddleware",
91 | ]
92 |
93 | if PRODUCTION:
94 | MIDDLEWARE.append("whitenoise.middleware.WhiteNoiseMiddleware")
95 |
96 | MIDDLEWARE += [
97 | "django.middleware.common.CommonMiddleware",
98 | "django.middleware.csrf.CsrfViewMiddleware",
99 | "django.contrib.auth.middleware.AuthenticationMiddleware",
100 | "django.contrib.messages.middleware.MessageMiddleware",
101 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
102 | ]
103 |
104 | ROOT_URLCONF = "server.urls"
105 |
106 | TEMPLATES = [
107 | {
108 | "BACKEND": "django.template.backends.django.DjangoTemplates",
109 | "DIRS": [BASE_DIR / "client" / "dist"],
110 | "APP_DIRS": True,
111 | "OPTIONS": {
112 | "context_processors": [
113 | "django.template.context_processors.debug",
114 | "django.template.context_processors.request",
115 | "django.contrib.auth.context_processors.auth",
116 | "django.contrib.messages.context_processors.messages",
117 | ],
118 | },
119 | },
120 | ]
121 |
122 | WSGI_APPLICATION = "server.wsgi.application"
123 |
124 |
125 | # Database
126 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases
127 |
128 | DATABASES = {"default": {**dj_database_url.config(), "ATOMIC_REQUESTS": True}}
129 |
130 |
131 | # Password validation
132 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
133 |
134 | AUTH_PASSWORD_VALIDATORS = [
135 | {
136 | "NAME": (
137 | "django.contrib.auth.password_validation"
138 | ".UserAttributeSimilarityValidator"
139 | ),
140 | },
141 | {
142 | "NAME": (
143 | "django.contrib.auth.password_validation.MinimumLengthValidator"
144 | ),
145 | },
146 | {
147 | "NAME": (
148 | "django.contrib.auth.password_validation.CommonPasswordValidator"
149 | ),
150 | },
151 | {
152 | "NAME": (
153 | "django.contrib.auth.password_validation.NumericPasswordValidator"
154 | ),
155 | },
156 | ]
157 |
158 |
159 | # Internationalization
160 | # https://docs.djangoproject.com/en/3.0/topics/i18n/
161 |
162 | LANGUAGE_CODE = "en-us"
163 |
164 | TIME_ZONE = os.getenv("TIME_ZONE", "UTC")
165 |
166 | USE_I18N = True
167 |
168 | USE_L10N = True
169 |
170 | USE_TZ = True
171 |
172 |
173 | # Static files (CSS, JavaScript, Images)
174 | # https://docs.djangoproject.com/en/3.0/howto/static-files/
175 |
176 | LOGIN_REDIRECT_URL = "/"
177 | LOGOUT_REDIRECT_URL = "/"
178 |
179 | # Logging
180 | LOGGING = {
181 | "version": 1,
182 | "disable_existing_loggers": False,
183 | "handlers": {
184 | "console": {
185 | "class": "logging.StreamHandler",
186 | },
187 | },
188 | "root": {
189 | "handlers": ["console"],
190 | "level": "INFO",
191 | },
192 | "loggers": {
193 | "django": {
194 | "handlers": ["console"],
195 | "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"),
196 | "propagate": False,
197 | },
198 | },
199 | }
200 |
201 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
202 |
--------------------------------------------------------------------------------
/home/tests/integration/intentionalwalk/test_create.py:
--------------------------------------------------------------------------------
1 | from django.test import Client, TestCase
2 |
3 |
4 | class ApiTestCase(TestCase):
5 | def setUp(self):
6 | # Test client
7 | self.client = Client()
8 | # Create a user
9 | response = self.client.post(
10 | path="/api/appuser/create",
11 | data={
12 | "name": "Abhay Kashyap",
13 | "email": "abhay@blah.com",
14 | "zip": "72185",
15 | "age": 99,
16 | "account_id": "12345",
17 | },
18 | content_type="application/json",
19 | )
20 |
21 | # Check for a successful response by the server
22 | self.assertEqual(response.status_code, 200)
23 | # Parse the response
24 | response_data = response.json()
25 | fail_message = f"Server response - {response_data}"
26 | self.assertEqual(response_data["status"], "success", msg=fail_message)
27 | self.assertEqual(
28 | response_data["message"],
29 | "Device registered & account registered successfully",
30 | msg=fail_message,
31 | )
32 |
33 | # Details for Daily walk even creation
34 | self.url = "/api/intentionalwalk/create"
35 | # Request parameters
36 | self.request_params = {
37 | "account_id": "12345",
38 | "intentional_walks": [
39 | {
40 | "event_id": "8888",
41 | "start": "2020-02-21T12:15:00-05:00",
42 | "end": "2020-02-21T12:45:00-05:00",
43 | "steps": 500,
44 | "distance": 1.3,
45 | "pause_time": 456,
46 | }
47 | ],
48 | }
49 | # Content type
50 | self.content_type = "application/json"
51 |
52 | # Test a successful creation of a intentional walk
53 | def test_create_intentionalwalk(self):
54 |
55 | # Send the request
56 | response = self.client.post(
57 | path=self.url,
58 | data=self.request_params,
59 | content_type=self.content_type,
60 | )
61 | # Check for a successful response by the server
62 | self.assertEqual(response.status_code, 200)
63 | # Parse the response
64 | response_data = response.json()
65 | fail_message = f"Server response - {response_data}"
66 | self.assertEqual(response_data["status"], "success", msg=fail_message)
67 | self.assertEqual(
68 | response_data["message"],
69 | "Intentional Walks recorded successfully",
70 | msg=fail_message,
71 | )
72 | self.assertEqual(
73 | response_data["payload"]["account_id"],
74 | self.request_params["account_id"],
75 | msg=fail_message,
76 | )
77 | self.assertEqual(
78 | response_data["payload"]["intentional_walks"][0]["event_id"],
79 | self.request_params["intentional_walks"][0]["event_id"],
80 | msg=fail_message,
81 | )
82 | self.assertEqual(
83 | response_data["payload"]["intentional_walks"][0]["start"],
84 | self.request_params["intentional_walks"][0]["start"],
85 | msg=fail_message,
86 | )
87 | self.assertEqual(
88 | response_data["payload"]["intentional_walks"][0]["end"],
89 | self.request_params["intentional_walks"][0]["end"],
90 | msg=fail_message,
91 | )
92 | self.assertEqual(
93 | response_data["payload"]["intentional_walks"][0]["steps"],
94 | self.request_params["intentional_walks"][0]["steps"],
95 | msg=fail_message,
96 | )
97 | self.assertEqual(
98 | response_data["payload"]["intentional_walks"][0]["pause_time"],
99 | self.request_params["intentional_walks"][0]["pause_time"],
100 | msg=fail_message,
101 | )
102 | self.assertEqual(
103 | response_data["payload"]["intentional_walks"][0]["distance"],
104 | self.request_params["intentional_walks"][0]["distance"],
105 | msg=fail_message,
106 | )
107 |
108 | # Test creation of a intentional walk with an invalid user account
109 | def test_create_intentionalwalk_invalidaccount(self):
110 |
111 | self.request_params["account_id"] = "0000000"
112 |
113 | # Send the request
114 | response = self.client.post(
115 | path=self.url,
116 | data=self.request_params,
117 | content_type=self.content_type,
118 | )
119 | # Check for a successful response by the server
120 | self.assertEqual(response.status_code, 200)
121 | # Parse the response
122 | response_data = response.json()
123 | fail_message = f"Server response - {response_data}"
124 | self.assertEqual(response_data["status"], "error", msg=fail_message)
125 | self.assertEqual(
126 | response_data["message"],
127 | "Unregistered device - "
128 | f'{self.request_params["account_id"]}.'
129 | " Please register first!",
130 | msg=fail_message,
131 | )
132 |
133 | # Test creation of a intentional walk with a missing field
134 | def test_create_intentionalwalk_missing_steps(self):
135 |
136 | del self.request_params["intentional_walks"][0]["steps"]
137 |
138 | # Send the request
139 | response = self.client.post(
140 | path=self.url,
141 | data=self.request_params,
142 | content_type=self.content_type,
143 | )
144 | # Check for a successful response by the server
145 | self.assertEqual(response.status_code, 200)
146 | # Parse the response
147 | response_data = response.json()
148 | fail_message = f"Server response - {response_data}"
149 | self.assertEqual(response_data["status"], "error", msg=fail_message)
150 | self.assertEqual(
151 | response_data["message"],
152 | "Required input 'steps' missing in the request",
153 | msg=fail_message,
154 | )
155 |
--------------------------------------------------------------------------------
/static/home/iw_plots.js:
--------------------------------------------------------------------------------
1 | // TODO: This objectively horrible, hacky, spaghetti code that must be refactored
2 | // Function to plot daily and cumulative user growths
3 | google.charts.load('current', {'packages':['bar', 'line']});
4 | google.charts.setOnLoadCallback(plot);
5 | function plot() {
6 |
7 | // --------------------------------- 1. Bar Plot - Recorded walks count ---------------------------------
8 | var data = new google.visualization.DataTable();
9 | data.addColumn('date', 'Date');
10 | data.addColumn('number', 'Recorded Walks');
11 | data.addRows(daily_recorded_walk_count)
12 | var options = {
13 | height: 400,
14 | legend: { position: 'none' },
15 | bar: { groupWidth: '95%' },
16 | vAxis: {
17 | title: "Recorded Walks",
18 | viewWindow: { min: 0 }
19 | },
20 | colors: ["#E59866"]
21 | };
22 | var chart = new google.charts.Bar(document.getElementById('daily_rw_count'));
23 | chart.draw(data, google.charts.Bar.convertOptions(options));
24 |
25 | // --------------------------------- 2. Line Plot - Recorded walks count ---------------------------------
26 | console.log(cumu_recorded_walk_count)
27 | var data = new google.visualization.DataTable();
28 | data.addColumn('date', 'Date');
29 | data.addColumn('number', 'Recorded Walks');
30 | data.addRows(cumu_recorded_walk_count)
31 | var options = {
32 | legend: { position: 'none' },
33 | height: 400,
34 | vAxis: {
35 | title: "Recorded Walks",
36 | viewWindow: { min: 0 }
37 | },
38 | colors: ["#E59866"]
39 | };
40 | var chart = new google.charts.Line(document.getElementById('total_rw_count'));
41 | chart.draw(data, google.charts.Line.convertOptions(options));
42 |
43 |
44 | // --------------------------------- 3. Bar Plot - Recorded walks Steps ---------------------------------
45 | var data = new google.visualization.DataTable();
46 | data.addColumn('date', 'Date');
47 | data.addColumn('number', 'Steps');
48 | data.addRows(daily_recorded_walk_steps)
49 | var options = {
50 | height: 400,
51 | legend: { position: 'none' },
52 | bar: { groupWidth: '95%' },
53 | vAxis: {
54 | title: "Steps",
55 | viewWindow: { min: 0 }
56 | },
57 | colors: ["#2ECC71"]
58 | };
59 | var chart = new google.charts.Bar(document.getElementById('daily_rw_steps'));
60 | chart.draw(data, google.charts.Bar.convertOptions(options));
61 |
62 | // --------------------------------- 4. Line Plot - Recorded walks Steps ---------------------------------
63 | var data = new google.visualization.DataTable();
64 | data.addColumn('date', 'Date');
65 | data.addColumn('number', 'Steps');
66 | data.addRows(cumu_recorded_walk_steps)
67 | var options = {
68 | legend: { position: 'none' },
69 | height: 400,
70 | vAxis: {
71 | title: "Steps",
72 | viewWindow: { min: 0 }
73 | },
74 | colors: ["#2ECC71"]
75 | };
76 | var chart = new google.charts.Line(document.getElementById('total_rw_steps'));
77 | chart.draw(data, google.charts.Line.convertOptions(options));
78 |
79 |
80 | // --------------------------------- 5. Bar Plot - Recorded walks Miles ---------------------------------
81 | var data = new google.visualization.DataTable();
82 | data.addColumn('date', 'Date');
83 | data.addColumn('number', 'Miles');
84 | data.addRows(daily_recorded_walk_miles)
85 | var options = {
86 | height: 400,
87 | legend: { position: 'none' },
88 | bar: { groupWidth: '95%' },
89 | vAxis: {
90 | title: "Steps",
91 | viewWindow: { min: 0 }
92 | },
93 | colors: ["#1ABC9C"]
94 | };
95 | var chart = new google.charts.Bar(document.getElementById('daily_rw_miles'));
96 | chart.draw(data, google.charts.Bar.convertOptions(options));
97 |
98 | // --------------------------------- 6. Line Plot - Recorded walks Miles ---------------------------------
99 | var data = new google.visualization.DataTable();
100 | data.addColumn('date', 'Date');
101 | data.addColumn('number', 'Miles');
102 | data.addRows(cumu_recorded_walk_miles)
103 | var options = {
104 | legend: { position: 'none' },
105 | height: 400,
106 | vAxis: {
107 | title: "Steps",
108 | viewWindow: { min: 0 }
109 | },
110 | colors: ["#1ABC9C"]
111 | };
112 | var chart = new google.charts.Line(document.getElementById('total_rw_miles'));
113 | chart.draw(data, google.charts.Line.convertOptions(options));
114 |
115 |
116 | // --------------------------------- 7. Bar Plot - Recorded walks Time ---------------------------------
117 | var data = new google.visualization.DataTable();
118 | data.addColumn('date', 'Date');
119 | data.addColumn('number', 'Minutes');
120 | data.addRows(daily_recorded_walk_time)
121 | var options = {
122 | height: 400,
123 | legend: { position: 'none' },
124 | bar: { groupWidth: '95%' },
125 | vAxis: {
126 | title: "Minutes",
127 | viewWindow: { min: 0 }
128 | },
129 | colors: ["#5DADE2"]
130 | };
131 | var chart = new google.charts.Bar(document.getElementById('daily_rw_time'));
132 | chart.draw(data, google.charts.Bar.convertOptions(options));
133 |
134 | // --------------------------------- 8. Line Plot - Recorded walks Steps ---------------------------------
135 | var data = new google.visualization.DataTable();
136 | data.addColumn('date', 'Date');
137 | data.addColumn('number', 'Minutes');
138 | data.addRows(cumu_recorded_walk_time)
139 | var options = {
140 | legend: { position: 'none' },
141 | height: 400,
142 | vAxis: {
143 | title: "Minutes",
144 | viewWindow: { min: 0 }
145 | },
146 | colors: ["#5DADE2"]
147 | };
148 | var chart = new google.charts.Line(document.getElementById('total_rw_time'));
149 | chart.draw(data, google.charts.Line.convertOptions(options));
150 |
151 | };
152 |
153 |
154 |
155 |
156 |
157 |
158 |
--------------------------------------------------------------------------------
/home/views/api/weeklygoal.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | from datetime import datetime
4 |
5 | from django.core.exceptions import ObjectDoesNotExist
6 | from django.http import JsonResponse
7 | from django.utils.decorators import method_decorator
8 | from django.views import View
9 | from django.views.decorators.csrf import csrf_exempt
10 |
11 | from home.models import WeeklyGoal, Device
12 | from home.utils.dates import get_start_of_week, DATE_FORMAT
13 |
14 |
15 | from .utils import validate_request_json
16 |
17 | logger = logging.getLogger(__name__)
18 |
19 |
20 | # Exempt from csrf validation
21 | @method_decorator(csrf_exempt, name="dispatch")
22 | class WeeklyGoalCreateView(View):
23 | """View to create or update a weeklygoal for an account"""
24 |
25 | model = WeeklyGoal
26 | http_method_names = ["post"]
27 |
28 | def post(self, request):
29 | json_data = json.loads(request.body)
30 |
31 | # Validate json. If any field is missing, send back the response message
32 | json_status = validate_request_json(
33 | json_data,
34 | required_fields=["account_id", "weekly_goal"],
35 | )
36 | if "status" in json_status and json_status["status"] == "error":
37 | return JsonResponse(json_status)
38 |
39 | # Validate weekly_goal json fields
40 | json_status = validate_request_json(
41 | json_data["weekly_goal"],
42 | required_fields=["start_of_week", "steps", "days"],
43 | )
44 |
45 | if "status" in json_status and json_status["status"] == "error":
46 | return JsonResponse(json_status)
47 |
48 | # Get the device
49 | try:
50 | device = Device.objects.get(device_id=json_data["account_id"])
51 | account = device.account
52 | except ObjectDoesNotExist:
53 | return JsonResponse(
54 | {
55 | "status": "error",
56 | "message": (
57 | "Unregistered device - "
58 | f"{json_data['account_id']}."
59 | " Please register first!"
60 | ),
61 | }
62 | )
63 |
64 | # Json response template
65 | json_response = {
66 | "status": "success",
67 | "message": "WeeklyGoal saved successfully",
68 | "payload": {
69 | "account_id": account.id,
70 | "weekly_goal": {},
71 | },
72 | }
73 |
74 | weekly_goal_update = json_data["weekly_goal"]
75 |
76 | start_of_week = weekly_goal_update["start_of_week"]
77 | start_of_week_update = get_start_of_week(
78 | datetime.strptime(start_of_week, DATE_FORMAT).date()
79 | )
80 | steps_update = weekly_goal_update["steps"]
81 | days_update = weekly_goal_update["days"]
82 |
83 | # Check if there's already a goal for the week. If there is,
84 | # update the entry.
85 | try:
86 | weekly_goal = WeeklyGoal.objects.get(
87 | account=account,
88 | start_of_week=start_of_week_update,
89 | )
90 | weekly_goal.steps = steps_update
91 | weekly_goal.days = days_update
92 | weekly_goal.save()
93 | except ObjectDoesNotExist:
94 | # Creation if object is missing
95 | weekly_goal = WeeklyGoal.objects.create(
96 | start_of_week=start_of_week_update,
97 | steps=steps_update,
98 | days=days_update,
99 | account=account,
100 | )
101 |
102 | # Update the json object
103 | json_response["payload"]["weekly_goal"] = {
104 | "start_of_week": weekly_goal.start_of_week,
105 | "steps": weekly_goal.steps,
106 | "days": weekly_goal.days,
107 | }
108 |
109 | return JsonResponse(json_response)
110 |
111 | def http_method_not_allowed(self, request):
112 | return JsonResponse(
113 | {"status": "error", "message": "Method not allowed!"}
114 | )
115 |
116 |
117 | @method_decorator(csrf_exempt, name="dispatch")
118 | class WeeklyGoalsListView(View):
119 | """View to retrieve Weekly Goals"""
120 |
121 | model = WeeklyGoal
122 | http_method_name = ["post"]
123 |
124 | def post(self, request):
125 | json_data = json.loads(request.body)
126 |
127 | # Validate json. If any field is missing, send back the response message
128 | json_status = validate_request_json(
129 | json_data, required_fields=["account_id"]
130 | )
131 |
132 | if "status" in json_status and json_status["status"] == "error":
133 | return JsonResponse(json_status)
134 |
135 | # Get the account
136 | try:
137 | device = Device.objects.get(device_id=json_data["account_id"])
138 | account = device.account
139 | except ObjectDoesNotExist:
140 | return JsonResponse(
141 | {
142 | "status": "error",
143 | "message": (
144 | "Unregistered account - "
145 | f"{json_data['account_id']}."
146 | " Please register first!"
147 | ),
148 | }
149 | )
150 |
151 | # Json response template
152 | json_response = {
153 | "status": "success",
154 | "message": "Weekly goals accessed successfully",
155 | "payload": {},
156 | }
157 |
158 | # Get weekly goals tied to this account
159 | weekly_goals = list(
160 | WeeklyGoal.objects.filter(account=account).values()
161 | )
162 | """ for goal in weekly_goals:
163 | goal = model_to_dict(goal) """
164 | for goal in weekly_goals:
165 | goal["start_of_week"] = goal["start_of_week"].strftime(DATE_FORMAT)
166 |
167 | json_response["payload"] = weekly_goals
168 |
169 | return JsonResponse(json_response)
170 |
171 | def http_method_not_allowed(self, request):
172 | return JsonResponse(
173 | {"status": "error", "message": "Method not allowed!"}
174 | )
175 |
--------------------------------------------------------------------------------
/home/tests/integration/appuser/test_delete.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ObjectDoesNotExist
2 | from django.test import Client, TestCase
3 |
4 |
5 | class ApiTestCase(TestCase):
6 | def setUp(self):
7 | # Test client
8 | self.client = Client()
9 |
10 | # Urls for creation and deletion
11 | self.create_url = "/api/appuser/create"
12 | self.del_url = "/api/appuser/delete"
13 | self.content_type = "application/json"
14 |
15 | # Constants
16 | self.account_id1 = "12345"
17 | self.account_id2 = "23456"
18 | self.email1 = "john@blah.com"
19 | self.email2 = "joe@blah.com"
20 |
21 | # Request parameters
22 | base_params = {
23 | "name": "John Doe",
24 | "zip": "72185",
25 | "age": 99,
26 | }
27 | self.request_params1 = base_params.copy()
28 | self.request_params2 = base_params.copy()
29 | self.request_params1.update(
30 | {
31 | "name": "John Doe",
32 | "email": self.email1,
33 | "account_id": self.account_id1,
34 | }
35 | )
36 | self.request_params2.update(
37 | {
38 | "name": "Joe Doe",
39 | "email": self.email2,
40 | "account_id": self.account_id2,
41 | }
42 | )
43 |
44 | # Register the users
45 | self.create_user_and_confirm_response(self.request_params1)
46 | self.create_user_and_confirm_response(self.request_params2)
47 |
48 | self.existing_user_accts = [self.account_id1, self.account_id2]
49 |
50 | def tearDown(self) -> None:
51 | for acct_id in self.existing_user_accts:
52 | response = self.client.delete(
53 | path=self.del_url,
54 | data={"account_id": acct_id},
55 | content_type=self.content_type,
56 | )
57 | self.check_delete_success(response, self.account_id1)
58 |
59 | def create_user_and_confirm_response(self, request_params):
60 | # Create the user
61 | response = self.client.post(
62 | path=self.create_url,
63 | data=request_params,
64 | content_type=self.content_type,
65 | )
66 |
67 | # Check for a successful response by the server
68 | self.assertEqual(response.status_code, 200)
69 | # Parse the response
70 | response_data = response.json()
71 | fail_message = f"Server response - {response_data}"
72 | self.assertEqual(response_data["status"], "success", msg=fail_message)
73 |
74 | def check_delete_success(self, response, deleted_account_id):
75 | # Check for a successful delete response by the server
76 | self.assertEqual(response.status_code, 200)
77 | # Parse the response
78 | response_data = response.json()
79 | fail_message = f"Server response - {response_data}"
80 | self.assertEqual(response_data["status"], "success", msg=fail_message)
81 | self.assertEqual(
82 | response_data["message"],
83 | "Account deleted successfully",
84 | msg=fail_message,
85 | )
86 |
87 | # Check user/device no longer exists by trying & failing to
88 | # update the nonexistent user
89 | with self.assertRaises(ObjectDoesNotExist):
90 | self.client.put(
91 | path=self.create_url,
92 | data={"account_id": deleted_account_id},
93 | content_type=self.content_type,
94 | )
95 |
96 | def check_users_still_exist(self, valid_account_ids=[]):
97 | # Check other users still exist
98 | for acct_id in valid_account_ids:
99 | expected_success_update_response = self.client.put(
100 | path=self.create_url,
101 | data={"account_id": acct_id},
102 | content_type=self.content_type,
103 | )
104 | # Check for a successful response by the server
105 | self.assertEqual(expected_success_update_response.status_code, 200)
106 | # Parse the response
107 | response_data = expected_success_update_response.json()
108 | fail_message = f"Server response - {response_data}"
109 | self.assertEqual(
110 | response_data["status"], "success", msg=fail_message
111 | )
112 | self.assertEqual(
113 | response_data["message"],
114 | "Account updated successfully",
115 | msg=fail_message,
116 | )
117 |
118 | def test_delete_user_success(self):
119 | # Delete the first user
120 | response = self.client.delete(
121 | path=self.del_url,
122 | data={"account_id": self.account_id1},
123 | content_type=self.content_type,
124 | )
125 |
126 | self.check_delete_success(response, self.account_id1)
127 | self.check_users_still_exist([self.account_id2])
128 |
129 | self.existing_user_accts = [self.account_id2]
130 |
131 | def check_delete_failure(self, response, expected_msg):
132 | # Check for a failed delete response by the server
133 | self.assertEqual(response.status_code, 200)
134 | # Parse the response
135 | response_data = response.json()
136 | fail_message = f"Server response - {response_data}"
137 | self.assertEqual(response_data["status"], "error", msg=fail_message)
138 | self.assertEqual(
139 | response_data["message"], expected_msg, msg=fail_message
140 | )
141 |
142 | def test_delete_user_failure_nonexistent(self):
143 | # Delete nonexistent user
144 | response = self.client.delete(
145 | path=self.del_url,
146 | data={"account_id": "fakeID"},
147 | content_type=self.content_type,
148 | )
149 |
150 | self.check_delete_failure(
151 | response,
152 | expected_msg="Cannot find user with specified account id.",
153 | )
154 | self.check_users_still_exist([self.account_id1, self.account_id2])
155 |
156 | def test_delete_user_failure_incorrect_params(self):
157 | # Send incorrect params
158 | response = self.client.delete(
159 | path=self.del_url,
160 | data={"what_is_this": self.account_id1},
161 | content_type=self.content_type,
162 | )
163 |
164 | self.check_delete_failure(
165 | response,
166 | expected_msg="Required input 'account_id' missing in the request",
167 | )
168 | self.check_users_still_exist([self.account_id1, self.account_id2])
169 |
--------------------------------------------------------------------------------