Skip to content

Commit 942d23a

Browse files
committed
feat: Add reports for DDDay.
1 parent 51b210d commit 942d23a

28 files changed

+52493
-370
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ staticfiles/
2626
# Ignore digital_meal folder
2727
digital_meal/
2828

29-
*/temp
29+
temp/

config/settings/base.py

+20-5
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,23 @@
194194

195195
# Reports
196196
# ------------------------------------------------------------------------------
197-
POLITICS_KEY_INSTAGRAM = os.getenv('POLITICS_KEY_INSTAGRAM', None)
198-
POLITICS_KEY_FACEBOOK = os.getenv('POLITICS_KEY_FACEBOOK', None)
199-
SEARCH_KEY = os.getenv('SEARCH_KEY', None)
200-
DIGITAL_MEAL_KEY = os.getenv('DIGITAL_MEAL_KEY', None)
201-
CHATGPT_KEY = os.getenv('CHATGPT_KEY', None)
197+
# Instagram Report
198+
INSTAGRAM_PROJECT_PK = os.getenv('INSTAGRAM_PROJECT_PK', None)
199+
INSTAGRAM_API_KEY = os.getenv('INSTAGRAM_API_KEY', None)
200+
BP_ID_FOLLOWED_ACCOUNTS = os.getenv('BP_ID_FOLLOWED_ACCOUNTS', None)
201+
202+
# Facebook Report
203+
FACEBOOK_PROJECT_PK = os.getenv('FACEBOOK_PROJECT_PK', None)
204+
FACEBOOK_API_KEY = os.getenv('FACEBOOK_API_KEY', None)
205+
206+
# Search Report
207+
SEARCH_PROJECT_PK = os.getenv('SEARCH_PROJECT_PK', None)
208+
SEARCH_API_KEY = os.getenv('SEARCH_API_KEY', None)
209+
210+
# Digital Meal Report
211+
DIGITALMEAL_PROJECT_PK = os.getenv('DIGITALMEAL_PROJECT_PK', None)
212+
DIGITALMEAL_API_KEY = os.getenv('DIGITALMEAL_API_KEY', None)
213+
214+
# ChatGPT Report
215+
CHATGPT_PROJECT_PK = os.getenv('CHATGPT_PROJECT_PK', None)
216+
CHATGPT_API_KEY = os.getenv('CHATGPT_API_KEY', None)

env.example

+20-5
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,23 @@ DJANGO_DB_USER=''
77
DJANGO_DB_PW=''
88

99
# Reports
10-
POLITICS_KEY_INSTAGRAM=''
11-
POLITICS_KEY_FACEBOOK=''
12-
SEARCH_KEY=''
13-
DIGITAL_MEAL_KEY=''
14-
CHATGPT_KEY=''
10+
## Instagram Report
11+
INSTAGRAM_PROJECT_PK=''
12+
INSTAGRAM_API_KEY=''
13+
BP_ID_FOLLOWED_ACCOUNTS=''
14+
15+
## Facebook Report
16+
FACEBOOK_PROJECT_PK=''
17+
FACEBOOK_API_KEY=''
18+
19+
## Search Report
20+
SEARCH_PROJECT_PK=''
21+
SEARCH_API_KEY=''
22+
23+
## Digital Meal Report
24+
DIGITALMEAL_PROJECT_PK=''
25+
DIGITALMEAL_API_KEY=''
26+
27+
## ChatGPT Report
28+
CHATGPT_PROJECT_PK=''
29+
CHATGPT_API_KEY=''

reports/migrations/0001_initial.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 3.2.17 on 2024-08-30 13:29
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
initial = True
9+
10+
dependencies = [
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='InstagramStatistics',
16+
fields=[
17+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+
('name', models.CharField(max_length=30)),
19+
('follow_counts', models.JSONField(default=None, null=True)),
20+
('biodiversity_counts', models.JSONField(default=None, null=True)),
21+
('pension_counts', models.JSONField(default=None, null=True)),
22+
('party_counts', models.JSONField(default=None, null=True)),
23+
('social_media_use', models.JSONField()),
24+
('last_updated', models.DateTimeField(blank=True, null=True)),
25+
('project_pk', models.IntegerField(default=0)),
26+
],
27+
),
28+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 3.2.17 on 2024-08-30 13:31
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('reports', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='instagramstatistics',
15+
name='social_media_use',
16+
field=models.JSONField(default=None, null=True),
17+
),
18+
]

reports/models.py

+99-31
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66
from ddm.models.core import DonationProject, QuestionnaireResponse, DataDonation, DonationBlueprint
77
from ddm.models.serializers import ResponseSerializer, DonationSerializer
88

9+
from .utils import insta_data
10+
911

1012
class InstagramStatistics (models.Model):
1113
name = models.CharField(max_length=30)
1214

15+
# Follow counts
16+
follow_counts = models.JSONField(default=None, null=True)
17+
1318
# Vote counts
1419
biodiversity_counts = models.JSONField(default=None, null=True)
1520
pension_counts = models.JSONField(default=None, null=True)
@@ -18,11 +23,41 @@ class InstagramStatistics (models.Model):
1823
party_counts = models.JSONField(default=None, null=True)
1924

2025
# Use counts
21-
social_media_use = models.JSONField()
26+
social_media_use = models.JSONField(default=None, null=True)
2227

2328
last_updated = models.DateTimeField(null=True, blank=True)
2429
project_pk = models.IntegerField(default=0)
2530

31+
def update_statistics(self):
32+
new_responses = self.get_responses()
33+
new_donations = self.get_blueprint_donations(settings.BP_ID_FOLLOWED_ACCOUNTS)
34+
35+
self.update_followed_accounts(new_donations)
36+
self.update_vote_counts(new_responses) # bio & pension
37+
self.update_party_graphs(new_responses, new_donations)
38+
self.last_updated = datetime.now()
39+
self.save()
40+
41+
def update_followed_accounts(self, donations=None, bp_pk=None):
42+
if donations is None:
43+
donations = self.get_blueprint_donations(settings.BP_ID_FOLLOWED_ACCOUNTS)
44+
45+
insta_accounts = insta_data.load_political_account_list()
46+
results = []
47+
for p, d in donations.items():
48+
data = {'Gefolgte Kanäle Instagram': [d]}
49+
followed_accounts = insta_data.get_follows_insta(data, insta_accounts)
50+
if followed_accounts:
51+
results.append(followed_accounts.copy())
52+
53+
if self.follow_counts is None:
54+
self.follow_counts = insta_data.TYPES_DICT_PLACEHOLDER.copy()
55+
56+
for r in results:
57+
for k in r.keys():
58+
self.follow_counts[k].append(len(r[k]))
59+
return
60+
2661
def update_vote_counts(self, responses=None):
2762
if responses is None:
2863
responses = self.get_responses()
@@ -51,10 +86,13 @@ def update_bio_count(self, responses, result_dummy, value_map):
5186
result = self.biodiversity_counts.copy()
5287

5388
var = 'vote-1'
54-
for response in responses:
55-
# TODO: Add check that participant has answered question; otherwise skip
56-
vote = response[var]
57-
result[value_map[vote]] += 1
89+
for p, r in responses.items():
90+
if var in r.keys():
91+
vote = r[var]
92+
else:
93+
continue
94+
if vote in value_map.keys():
95+
result[value_map[vote]] += 1
5896
self.biodiversity_counts = result
5997
return
6098

@@ -65,10 +103,13 @@ def update_pension_count(self, responses, result_dummy, value_map):
65103
result = self.pension_counts.copy()
66104

67105
var = 'vote-2'
68-
for response in responses:
69-
# TODO: Add check that participant has answered question; otherwise skip
70-
vote = response[var]
71-
result[value_map[vote]] += 1
106+
for p, r in responses.items():
107+
if var in r.keys():
108+
vote = r[var]
109+
else:
110+
continue
111+
if vote in value_map.keys():
112+
result[value_map[vote]] += 1
72113
self.pension_counts = result
73114
return
74115

@@ -88,25 +129,40 @@ def update_party_graphs(self, responses=None, donations=None, bp_pk=None):
88129
'FDP': scale_dummy.copy()
89130
}
90131

91-
for response in responses:
92-
# get participant id
93-
participant = None
94-
# Get var political left/right
95-
var = 'lrsp'
96-
# Check if participant has answered the question
97-
# TODO: Add check that participant has answered question; otherwise skip
98-
pol_stance = response[var]
132+
for participant, response in responses.items():
133+
if not (participant in responses.keys() and participant in donations.keys()):
134+
continue
99135

100-
# get donation belonging to response
101-
response_donation = None
102-
# compute
103-
104-
105-
# SP
106-
# Mitte
107-
# SVP
108-
# FDP
109-
pass
136+
var = 'lrsp'
137+
if var in response.keys():
138+
pol_stance = response[var]
139+
else:
140+
continue
141+
142+
valid_responses = [str(i) for i in range(1, 11)]
143+
if pol_stance not in valid_responses:
144+
continue
145+
146+
donation = donations[participant]
147+
political_accounts = insta_data.load_political_account_list()
148+
parties = ['SP', 'SVP', 'Mitte', 'FDP']
149+
p_follows_party = {p: False for p in parties}
150+
for account in donation:
151+
profile = account['string_list_data'][0]['href']
152+
if profile in political_accounts.keys():
153+
insta_profile = political_accounts[profile]
154+
profile_type = insta_profile['type']
155+
if profile_type != 'party':
156+
continue
157+
profile_party = insta_profile['party']
158+
if profile_party in parties:
159+
p_follows_party[profile_party] = True
160+
161+
# Add to result
162+
for party, follows in p_follows_party.items():
163+
if follows:
164+
self.party_counts[party][pol_stance] += 1
165+
return
110166

111167
def update_sm_use(self, responses=None):
112168
var = 'media_use-4'
@@ -125,18 +181,28 @@ def get_decryptor(self, project):
125181
return Decryption(settings.SECRET_KEY, project.get_salt())
126182

127183
def get_responses(self):
184+
"""
185+
Returns dictionary with responses per participant.
186+
{'participant_id': {'response-var': <response>, ...}}
187+
"""
128188
project = self.get_project()
129189
reference_date = self.get_reference_date()
130190

131191
responses = QuestionnaireResponse.objects.filter(
132192
project=project, time_submitted__gte=reference_date)
133193

134194
decryptor = self.get_decryptor(project)
135-
decrypted_responses = [ResponseSerializer(r, decryptor=decryptor).data['responses'] for r in responses]
136-
195+
decrypted_responses = {}
196+
for r in responses:
197+
serialized_r = ResponseSerializer(r, decryptor=decryptor)
198+
decrypted_responses[serialized_r.data['participant']] = serialized_r.data['responses']
137199
return decrypted_responses
138200

139201
def get_blueprint_donations(self, bp_pk):
202+
"""
203+
Returns dictionary with donations per participant.
204+
{'participant_id': <extracted donation>}
205+
"""
140206
project = self.get_project()
141207
reference_date = self.get_reference_date()
142208
blueprint = DonationBlueprint.objects.get(pk=bp_pk)
@@ -145,6 +211,8 @@ def get_blueprint_donations(self, bp_pk):
145211
blueprint=blueprint, time_submitted__gte=reference_date)
146212

147213
decryptor = self.get_decryptor(project)
148-
decrypted_donations = [DonationSerializer(d, decryptor=decryptor).data['data'] for d in donations]
149-
214+
decrypted_donations = {}
215+
for d in donations:
216+
serialized_d = DonationSerializer(d, decryptor=decryptor)
217+
decrypted_donations[serialized_d.data['participant']] = serialized_d.data['data']
150218
return decrypted_donations

reports/plots/__init__.py

Whitespace-only changes.

reports/plots/search.py

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from bokeh.embed import components
2+
from bokeh.plotting import figure
3+
4+
5+
FIRE_PALETTE = ['#de0c1c', '#fe2d2d', '#fb7830', '#fecf02'] #, '#ffeea3']
6+
7+
8+
def get_language_plot(data, bar_color='#000'):
9+
"""
10+
data = {'posemo': 2.060499780797895, 'negemo': 0.7452871547566856, 'anx': 0.131521262604121,
11+
'anger': 0.08768084173608066, 'sad': 0.5260850504164841, 'social': 3.9017974572555874,
12+
'cogproc': 9.294169224024545, 'bio': 0.7891275756247259, 'body': 0.08768084173608066},
13+
"""
14+
original_categories = ['posemo', 'negemo', 'anx', 'anger', 'sad'] #, 'social', 'cogproc', 'bio', 'body']
15+
new_names = {
16+
'posemo': 'Positive Emotionen',
17+
'negemo': 'Negative Emotionen',
18+
'anx': 'Angst',
19+
'anger': 'Wut',
20+
'sad': 'Traurigkeit',
21+
'social': 'Sozial (?)',
22+
'cogproc': 'Kognitive Prozesse (?)',
23+
'bio': 'Physisch (?)',
24+
'body': 'Körper (?)'
25+
}
26+
27+
categories = [new_names[c] for c in original_categories]
28+
values = [data[c] for c in original_categories]
29+
30+
p = figure(x_range=categories, height=400, width=400,
31+
toolbar_location=None, tools="")
32+
p.vbar(x=categories, top=values, width=0.9,
33+
fill_color=bar_color, line_color=bar_color)
34+
35+
p.border_fill_color = None
36+
p.y_range.start = 0
37+
p.x_range.range_padding = 0.1
38+
p.xaxis.major_label_orientation = 1
39+
p.axis.minor_tick_line_color = None
40+
p.xgrid.grid_line_color = None
41+
p.ygrid.grid_line_color = None
42+
43+
p.xaxis.group_text_color = '#000'
44+
p.xaxis.group_text_font_size = '10pt'
45+
46+
p.xaxis.axis_label = 'Emotionalität Ihrer Suchanfragen'
47+
p.yaxis.axis_label = f'Score'
48+
p.xaxis.axis_label_text_font_size = '11pt'
49+
p.yaxis.axis_label_text_font_size = '9pt'
50+
p.xaxis.axis_label_text_font_style = 'normal'
51+
p.yaxis.axis_label_text_font_style = 'normal'
52+
p.xaxis.major_label_text_font_size = '10pt'
53+
p.xaxis.major_tick_line_color = None
54+
55+
script, div = components(p)
56+
return {'script': script, 'div': div}

0 commit comments

Comments
 (0)