├── forms ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0026_remove_internetnewssheet_person_secondary.py │ ├── 0010_televisionsheet_comments.py │ ├── 0007_auto_20150309_1422.py │ ├── 0005_internetnewsperson_age.py │ ├── 0008_auto_20150309_1902.py │ ├── 0039_auto_20200120_1536.py │ ├── 0040_auto_20200120_1538.py │ ├── 0041_auto_20200123_1224.py │ ├── 0045_auto_20200306_1510.py │ ├── 0029_auto_20191210_2042.py │ ├── 0030_auto_20191210_2043.py │ ├── 0033_auto_20191212_2003.py │ ├── 0006_auto_20150309_1403.py │ ├── 0053_add_monitoring_mode_20200909_1013.py │ ├── 0013_auto_20150324_0700.py │ ├── 0012_auto_20150312_1400.py │ ├── 0011_auto_20150312_1358.py │ ├── 0057_add_monitor_code.py │ ├── 0054_monitoring_mode_messages_20200910_1441.py │ ├── 0017_auto_20150331_1815.py │ ├── 0060_add_moldova_to_country_region.py │ ├── 0036_auto_20200114_1337.py │ ├── 0022_assign_country_region_to_sheet_models.py │ ├── 0044_auto_20200226_0758.py │ ├── 0025_auto_20191210_1924.py │ ├── 0002_auto_20150309_1227.py │ ├── 0009_auto_20150312_1347.py │ ├── 0050_add_created_and_updated_at.py │ ├── 0049_auto_20200820_0739.py │ ├── 0059_update_country_region.py │ ├── 0043_update_country_region.py │ ├── 0058_add_deleted_attribute.py │ ├── 0003_auto_20150309_1340.py │ ├── 0018_auto_20150416_0855.py │ ├── 0019_auto_20150506_1052.py │ ├── 0004_auto_20150309_1358.py │ ├── 0051_increate_people_in_the_news_age.py │ ├── 0046_auto_20200312_0949.py │ ├── 0042_auto_20200220_1247.py │ └── 0020_auto_20150506_1136.py ├── tests.py ├── views.py └── static │ └── forms │ └── admin │ ├── check_tab_errors │ ├── radioperson.js │ ├── newspaperperson.js │ ├── televisionperson.js │ ├── internetnewsperson.js │ └── check.js │ ├── move_fields.js │ ├── move_radio_fields.js │ ├── move_fields_newspaper.js │ ├── move_twitter_fields.js │ ├── move_television_fields.js │ └── move_internet_fields.js ├── reports ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── import-weights.py │ │ ├── generate_dataset.py │ │ └── import-historical.py ├── migrations │ ├── __init__.py │ ├── 0006_unique_country_mediatype.py │ ├── 0001_initial.py │ ├── 0009_delete_tv_media_type.py │ ├── 0003_indonesia-weights.py │ ├── 0004_transnational_weights.py │ ├── 0008_gsheetcountryweights.py │ └── 0005_duplicate_weights.py ├── tests.py ├── admin.py ├── apps.py ├── historical │ ├── __init__.py │ ├── canon.py │ ├── historical.py │ └── _base_importer.py ├── urls.py ├── signals.py ├── models.py ├── forms.py └── views.py ├── static ├── .placeholder └── js │ └── tabs.js ├── gmmp ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── write_weights_to_dict.py │ │ ├── fix_countries.py │ │ ├── fix_regions.py │ │ ├── bulk_password_reset_emails.py │ │ ├── sync_users.py │ │ └── map_weights_to_codes.py ├── migrations │ ├── __init__.py │ ├── 0003_auto_20200114_1330.py │ ├── 0002_auto_20191204_1908.py │ ├── 0005_special_questions_gsheet_integration.py │ ├── 0001_initial.py │ ├── 0004_specialquestions.py │ └── 0006_countryuser.py ├── __init__.py ├── static │ ├── help.pdf │ ├── favicon.ico │ ├── img │ │ ├── wmtn.png │ │ ├── internet.png │ │ ├── gmmp │ │ │ ├── full.png │ │ │ ├── logo.png │ │ │ └── text.png │ │ └── newspaper.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-150x150.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── wazimap │ │ ├── favicon.5debd593.ico │ │ ├── loading.a08ac827.gif │ │ ├── slide-1.56cb412e.jpg │ │ ├── slide-2.940fd1c6.jpg │ │ ├── slide-2b.f364dcbf.jpg │ │ ├── slide-3.874979e5.jpg │ │ ├── slide-4.daa3644d.jpg │ │ ├── slide-5.f9daed8f.jpg │ │ ├── slide-7.9eecdd38.jpg │ │ ├── webclip.7bba710e.png │ │ ├── js.cba122a9.css │ │ ├── js.cba122a9.css.map │ │ └── normalize.83e55e48.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── css │ │ ├── login.css │ │ ├── 1-col-portfolio.css │ │ ├── gmmp.css │ │ └── 3-col-portfolio.css │ ├── browserconfig.xml │ └── site.webmanifest ├── apps.py ├── wsgi.py ├── templates │ ├── gmmp │ │ └── dashboard_modules │ │ │ ├── submissions.html │ │ │ └── add_submission.html │ ├── admin │ │ ├── change_form.html │ │ ├── _footer.html │ │ ├── submit_line.html │ │ ├── login.html │ │ └── base_site.html │ ├── data_export.html │ ├── report_filter.html │ ├── base.html │ └── index.html ├── middlewares.py ├── views.py ├── dashboard.py ├── admin.py ├── templatetags │ └── i18n_switcher.py ├── models.py ├── signals.py ├── urls.py ├── dashboard_modules.py └── locale │ └── en │ └── LC_MESSAGES │ └── django.po ├── runtime.txt ├── requirements.txt ├── requirements-dev.txt ├── .env └── .env.template ├── contrib └── docker │ ├── entrypoint.sh │ └── cmd.sh ├── manage.py ├── requirements-all.txt ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ └── main.yml └── PULL_REQUEST_TEMPLATE.md ├── README.md ├── docker-compose.yml └── Dockerfile /forms/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /reports/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /forms/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gmmp/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gmmp/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.0 2 | -------------------------------------------------------------------------------- /reports/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /reports/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gmmp/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /reports/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gmmp/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'gmmp.app.GmmpConfig' 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements-all.txt 2 | psycopg2==2.8.6 3 | -------------------------------------------------------------------------------- /forms/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /reports/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /forms/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /reports/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements-all.txt 2 | black==20.8b1 3 | psycopg2-binary==2.8.6 4 | -------------------------------------------------------------------------------- /gmmp/static/help.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/help.pdf -------------------------------------------------------------------------------- /gmmp/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/favicon.ico -------------------------------------------------------------------------------- /gmmp/static/img/wmtn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/img/wmtn.png -------------------------------------------------------------------------------- /gmmp/static/img/internet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/img/internet.png -------------------------------------------------------------------------------- /gmmp/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/favicon-16x16.png -------------------------------------------------------------------------------- /gmmp/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/favicon-32x32.png -------------------------------------------------------------------------------- /gmmp/static/img/gmmp/full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/img/gmmp/full.png -------------------------------------------------------------------------------- /gmmp/static/img/gmmp/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/img/gmmp/logo.png -------------------------------------------------------------------------------- /gmmp/static/img/gmmp/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/img/gmmp/text.png -------------------------------------------------------------------------------- /gmmp/static/img/newspaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/img/newspaper.png -------------------------------------------------------------------------------- /gmmp/static/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/mstile-150x150.png -------------------------------------------------------------------------------- /gmmp/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/apple-touch-icon.png -------------------------------------------------------------------------------- /gmmp/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /gmmp/static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /.env/.env.template: -------------------------------------------------------------------------------- 1 | SITE_URL= 2 | GMMP_EMAIL_HOST_PASSWORD= 3 | GMMP_EMAIL_FROM= 4 | GSHEETS_WEIGHTS_SPREADSHEET_ID= 5 | SENTRY_DNS= 6 | -------------------------------------------------------------------------------- /gmmp/static/wazimap/favicon.5debd593.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/wazimap/favicon.5debd593.ico -------------------------------------------------------------------------------- /gmmp/static/wazimap/loading.a08ac827.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/wazimap/loading.a08ac827.gif -------------------------------------------------------------------------------- /gmmp/static/wazimap/slide-1.56cb412e.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/wazimap/slide-1.56cb412e.jpg -------------------------------------------------------------------------------- /gmmp/static/wazimap/slide-2.940fd1c6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/wazimap/slide-2.940fd1c6.jpg -------------------------------------------------------------------------------- /gmmp/static/wazimap/slide-2b.f364dcbf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/wazimap/slide-2b.f364dcbf.jpg -------------------------------------------------------------------------------- /gmmp/static/wazimap/slide-3.874979e5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/wazimap/slide-3.874979e5.jpg -------------------------------------------------------------------------------- /gmmp/static/wazimap/slide-4.daa3644d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/wazimap/slide-4.daa3644d.jpg -------------------------------------------------------------------------------- /gmmp/static/wazimap/slide-5.f9daed8f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/wazimap/slide-5.f9daed8f.jpg -------------------------------------------------------------------------------- /gmmp/static/wazimap/slide-7.9eecdd38.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/wazimap/slide-7.9eecdd38.jpg -------------------------------------------------------------------------------- /gmmp/static/wazimap/webclip.7bba710e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/wazimap/webclip.7bba710e.png -------------------------------------------------------------------------------- /gmmp/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /gmmp/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /gmmp/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /gmmp/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeForAfrica/gmmp/master/gmmp/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /gmmp/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class GmmpConfig(AppConfig): 5 | name = "gmmp" 6 | 7 | def ready(self): 8 | import gmmp.signals 9 | -------------------------------------------------------------------------------- /reports/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ReportsConfig(AppConfig): 5 | name = "reports" 6 | 7 | def ready(self): 8 | import reports.signals 9 | -------------------------------------------------------------------------------- /gmmp/wsgi.py: -------------------------------------------------------------------------------- 1 | from django.core.wsgi import get_wsgi_application 2 | import os 3 | 4 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gmmp.settings') 5 | application = get_wsgi_application() 6 | -------------------------------------------------------------------------------- /forms/static/forms/admin/check_tab_errors/radioperson.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | $(document).ready(function() { 3 | check_tab_errors($, '#radioperson_set-group', 6); 4 | }); 5 | }(jet.jQuery)); 6 | -------------------------------------------------------------------------------- /forms/static/forms/admin/check_tab_errors/newspaperperson.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | $(document).ready(function() { 3 | check_tab_errors($, '#newspaperperson_set-group', 6); 4 | }); 5 | }(jet.jQuery)); 6 | -------------------------------------------------------------------------------- /forms/static/forms/admin/check_tab_errors/televisionperson.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | $(document).ready(function() { 3 | check_tab_errors($, '#televisionperson_set-group', 6); 4 | }); 5 | }(jet.jQuery)); 6 | -------------------------------------------------------------------------------- /forms/static/forms/admin/check_tab_errors/internetnewsperson.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | $(document).ready(function() { 3 | check_tab_errors($, '#internetnewsperson_set-group', 6); 4 | }); 5 | }(jet.jQuery)); 6 | -------------------------------------------------------------------------------- /reports/historical/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["historical", "canon"] 2 | 3 | # Preserve the current `from reports.historical import Historical, canon` syntax 4 | from .historical import Historical 5 | from .canon import canon 6 | -------------------------------------------------------------------------------- /forms/static/forms/admin/check_tab_errors/check.js: -------------------------------------------------------------------------------- 1 | var check_tab_errors = function($, tab_id, tab_number) { 2 | if ($(tab_id + ' *').hasClass('errorlist')) { 3 | $('.changeform-tabs li:nth-child(' + tab_number + ')').addClass('errors'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /reports/historical/canon.py: -------------------------------------------------------------------------------- 1 | from ._recodes import recode 2 | 3 | 4 | def canon(key): 5 | if key: 6 | canon_key = key.replace("…", "") 7 | canon_key = recode(canon_key.strip()) 8 | return canon_key.strip().lower() 9 | return "" 10 | -------------------------------------------------------------------------------- /gmmp/static/css/login.css: -------------------------------------------------------------------------------- 1 | body.login { 2 | padding-top: 10px; 3 | } 4 | body.login .img { 5 | width: 100%; 6 | } 7 | body.login .login-form { 8 | margin-bottom: 2rem; 9 | } 10 | body.login .submit-row input[type="submit"] { 11 | background-color: #29abe2; 12 | } 13 | -------------------------------------------------------------------------------- /contrib/docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$DATABASE" = "postgres" ] 4 | then 5 | echo "Waiting for postgres..." 6 | 7 | while ! nc -z $SQL_HOST $SQL_PORT; do 8 | sleep 0.1 9 | done 10 | 11 | echo "PostgreSQL started" 12 | fi 13 | 14 | exec "$@" 15 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gmmp.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /forms/static/forms/admin/move_fields.js: -------------------------------------------------------------------------------- 1 | var move_journalist = function($, group_id) { 2 | var a = $(group_id) 3 | $('.journalists-fieldset').append(a) 4 | } 5 | 6 | var move_people = function($, group_id) { 7 | var a = $(group_id) 8 | $('.people-fieldset').append(a) 9 | } 10 | 11 | -------------------------------------------------------------------------------- /gmmp/static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #29abe2 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /forms/static/forms/admin/move_radio_fields.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | $(document).ready(function() { 3 | move_journalist($, "#radiojournalist_set-group"); 4 | move_people($, "#radioperson_set-group"); 5 | $(".changeform-tabs > li").last().remove() 6 | $(".changeform-tabs > li").last().remove() 7 | }); 8 | }(jet.jQuery)); 9 | 10 | -------------------------------------------------------------------------------- /forms/static/forms/admin/move_fields_newspaper.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | $(document).ready(function() { 3 | move_journalist($, "#newspaperjournalist_set-group"); 4 | move_people($, "#newspaperperson_set-group"); 5 | $(".changeform-tabs > li").last().remove() 6 | $(".changeform-tabs > li").last().remove() 7 | }); 8 | }(jet.jQuery)); 9 | -------------------------------------------------------------------------------- /forms/static/forms/admin/move_twitter_fields.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | $(document).ready(function() { 3 | move_journalist($, "#twitterjournalist_set-group"); 4 | move_people($, "#twitterperson_set-group"); 5 | $(".changeform-tabs > li").last().remove() 6 | $(".changeform-tabs > li").last().remove() 7 | }); 8 | }(jet.jQuery)); 9 | 10 | -------------------------------------------------------------------------------- /gmmp/templates/gmmp/dashboard_modules/submissions.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /forms/static/forms/admin/move_television_fields.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | $(document).ready(function() { 3 | move_journalist($, "#televisionjournalist_set-group"); 4 | move_people($, "#televisionperson_set-group"); 5 | $(".changeform-tabs > li").last().remove() 6 | $(".changeform-tabs > li").last().remove() 7 | }); 8 | }(jet.jQuery)); 9 | 10 | -------------------------------------------------------------------------------- /forms/static/forms/admin/move_internet_fields.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | $(document).ready(function() { 3 | move_journalist($, "#internetnewsjournalist_set-group"); 4 | move_people($, "#internetnewsperson_set-group"); 5 | $(".changeform-tabs > li").last().remove() 6 | $(".changeform-tabs > li").last().remove() 7 | }); 8 | }(jet.jQuery)); 9 | 10 | -------------------------------------------------------------------------------- /requirements-all.txt: -------------------------------------------------------------------------------- 1 | Django==2.2.28 2 | XlsxWriter==1.3.7 3 | dj-database-url==0.5.0 4 | dj-static==0.0.6 5 | django-countries==7.1.0 6 | django-debug-toolbar== 3.2.1 7 | django-extensions==3.1.3 8 | django-jet==1.0.8 9 | django-gsheets==0.0.10 10 | django-guardian==2.3.0 11 | gunicorn[gevent]==20.1.0 12 | openpyxl==3.0.7 13 | python-dotenv==0.17.0 14 | sentry-sdk==1.14.0 15 | whitenoise==5.0.1 16 | -------------------------------------------------------------------------------- /gmmp/templates/gmmp/dashboard_modules/add_submission.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {% for sheet in module.children %} 4 |
5 |

{{sheet.description}}

6 |
7 | 8 | 9 | {{ sheet.title }} 10 | 11 | {% endfor %} 12 |
13 | -------------------------------------------------------------------------------- /reports/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.views.generic.base import RedirectView 3 | from reports.views import ReportView, data_export 4 | 5 | urlpatterns = [ 6 | path('', ReportView.as_view(), name='get_xlsx_reports'), 7 | path('data_export/', data_export, name='get_data_export'), 8 | path('wazimap', RedirectView.as_view(url='/genmap', permanent=True), name='wazimap'), 9 | ] 10 | -------------------------------------------------------------------------------- /gmmp/middlewares.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import redirect 2 | from whitenoise.middleware import WhiteNoiseMiddleware 3 | 4 | 5 | class ProtectedStaticFileMiddleware(WhiteNoiseMiddleware): 6 | def process_request(self, request): 7 | # check user authentication 8 | if not request.path.startswith('/static/wazimap/') or request.user.is_authenticated: 9 | return WhiteNoiseMiddleware().process_request(request) 10 | return redirect('wazimap') 11 | -------------------------------------------------------------------------------- /reports/management/commands/import-weights.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from reports.models import GSheetCountryWeights 4 | 5 | 6 | class Command(BaseCommand): 7 | help = "Import weights from a gSheet and update DB" 8 | 9 | def handle(self, *args, **options): 10 | # NOTE(kilemensi): Don't call `syncgsheets` since it auto discovers 11 | # and sync **all** models 12 | GSheetCountryWeights.pull_sheet() 13 | -------------------------------------------------------------------------------- /gmmp/management/commands/write_weights_to_dict.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from pprint import pprint 3 | 4 | from django.core.management.base import BaseCommand 5 | 6 | class Command(BaseCommand): 7 | def handle(self, *args, **options): 8 | with open(args[0]) as csvfile: 9 | reader = csv.DictReader(csvfile) 10 | weights = [] 11 | for row in reader: 12 | row['Twitter'] = 1 13 | weights.append(row) 14 | pprint(weights) 15 | -------------------------------------------------------------------------------- /forms/migrations/0026_remove_internetnewssheet_person_secondary.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0025_auto_20191210_1924'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='internetnewssheet', 16 | name='person_secondary', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /gmmp/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Global Media Monitoring Project", 3 | "short_name": "GMMP", 4 | "icons": [ 5 | { 6 | "src": "/static/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/static/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /gmmp/static/css/1-col-portfolio.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - 1 Col Portfolio HTML Template (http://startbootstrap.com) 3 | * Code licensed under the Apache License v2.0. 4 | * For details, see http://www.apache.org/licenses/LICENSE-2.0. 5 | */ 6 | 7 | body { 8 | padding-top: 70px; /* Required padding for .navbar-fixed-top. Remove if using .navbar-static-top. Change if height of navigation changes. */ 9 | } 10 | 11 | footer { 12 | margin: 50px 0; 13 | } 14 | 15 | .navbar-brand { 16 | margin-top: -11px; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /reports/migrations/0006_unique_country_mediatype.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.18 on 2021-02-18 12:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('reports', '0005_duplicate_weights'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddConstraint( 14 | model_name='weights', 15 | constraint=models.UniqueConstraint(fields=('country', 'media_type'), name='country_media_type_key'), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /gmmp/migrations/0003_auto_20200114_1330.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.9 on 2020-01-14 13:30 2 | 3 | from django.db import migrations 4 | import django_countries.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('gmmp', '0002_auto_20191204_1908'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='monitor', 16 | name='country', 17 | field=django_countries.fields.CountryField(default='KE', max_length=2), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /gmmp/migrations/0002_auto_20191204_1908.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django_countries.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('gmmp', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='monitor', 17 | name='country', 18 | field=django_countries.fields.CountryField(default=b'KE', max_length=2), 19 | preserve_default=True, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /gmmp/static/wazimap/js.cba122a9.css: -------------------------------------------------------------------------------- 1 | .container{border:1px solid #000;width:800px;height:100px;margin-top:50px;margin-left:50px}.d3-tip{line-height:1;padding:8px;background:rgba(0,0,0,.8);color:#fff;border-radius:4px;font-size:16px;pointer-events:none}.d3-tip.n:after{content:"\25BC";margin:-2px 0 0;top:100%}.d3-tip.n:after,.d3-tip.s:before{box-sizing:border-box;display:inline;font-size:10px;width:100%;line-height:1;color:rgba(0,0,0,.8);position:absolute;text-align:center;left:0}.d3-tip.s:before{content:"\25B2";margin:0 0 -2px;bottom:100%}.indicator__chart_container rect.bar{fill:#e4653d} 2 | /*# sourceMappingURL=js.cba122a9.css.map */ -------------------------------------------------------------------------------- /gmmp/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import views as auth_views 2 | from gmmp import settings 3 | 4 | 5 | class CustomPassowrdResetView(auth_views.PasswordResetView): 6 | def get_context_data(self, **kwargs): 7 | context = super().get_context_data(**kwargs) 8 | context['site_header'] = settings.ADMIN_SITE_SITE_HEADER 9 | return context 10 | 11 | class CustomPasswordResetDoneView(auth_views.PasswordResetDoneView): 12 | def get_context_data(self, **kwargs): 13 | context = super().get_context_data(**kwargs) 14 | context['site_header'] = settings.ADMIN_SITE_SITE_HEADER 15 | return context 16 | -------------------------------------------------------------------------------- /forms/migrations/0010_televisionsheet_comments.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0009_auto_20150312_1347'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='televisionsheet', 16 | name='comments', 17 | field=models.TextField(verbose_name='Describe any photographs included in the story and the conclusions you draw from them.', blank=True), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /forms/migrations/0007_auto_20150309_1422.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0006_auto_20150309_1403'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RenameField( 15 | model_name='televisionsheet', 16 | old_name='television_station', 17 | new_name='television_channel', 18 | ), 19 | migrations.RemoveField( 20 | model_name='televisionsheet', 21 | name='person_secondary', 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /gmmp/templates/admin/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n admin_urls %} 3 | 4 | {% block breadcrumbs %} 5 | 11 | {% endblock %} 12 | 13 | -------------------------------------------------------------------------------- /gmmp/templates/data_export.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block page_content %} 5 | 6 |
7 |
8 |

9 |

Data export

10 |

Click on the button below to export the data.

11 |
12 |
13 |
14 |
15 |
16 | {% csrf_token %} 17 | 18 |
19 |
20 | {% endblock page_content %} 21 | -------------------------------------------------------------------------------- /forms/migrations/0005_internetnewsperson_age.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0004_auto_20150309_1358'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='internetnewsperson', 16 | name='age', 17 | field=models.PositiveIntegerField(default=None, verbose_name='Age (person appears)', choices=[(0, 'Do not know'), (1, '12 and under'), (2, '13-18'), (3, '19-34'), (4, '35-49'), (5, '50-64'), (6, '65 years or more')]), 18 | preserve_default=False, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /forms/migrations/0008_auto_20150309_1902.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0007_auto_20150309_1422'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='televisionsheet', 16 | name='television_channel', 17 | field=models.CharField(help_text="Be as specific as possible. E.g. if the television company is called RTV, and if the newscast is broadcast on its second channel, write in 'RTV-2' ", max_length=255, verbose_name='Channel'), 18 | preserve_default=True, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /gmmp/management/commands/fix_countries.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django.db.models import F 3 | from forms.models import sheet_models 4 | 5 | class Command(BaseCommand): 6 | def handle(self, *args, **options): 7 | for name, model in sheet_models.items(): 8 | country_errors_sheets = model.objects.exclude(monitor__country__in=F('country')) 9 | for sheet in country_errors_sheets: 10 | try: 11 | sheet.country = sheet.monitor.country 12 | sheet.save() 13 | self.stdout.write("%s,%s" % (name, sheet.id)) 14 | except AttributeError: 15 | self.stdout.write("Sheet has no monitor: %s %s" % (name, sheet.id)) 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Dokku CD 2 | on: 3 | push: 4 | branches: [ master ] 5 | env: 6 | DOKKU_APP_NAME: 'gmmp' 7 | DOKKU_HOST: 'dokku-1.hurumap.org' 8 | DOKKU_REMOTE_BRANCH: 'master' 9 | GIT_PUSH_FLAGS: '--force' 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | - id: deploy 18 | name: Deploy to dokku 19 | uses: idoberko2/dokku-deploy-github-action@v1 20 | with: 21 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 22 | dokku-host: ${{ env.DOKKU_HOST }} 23 | app-name: ${{ env.DOKKU_APP_NAME }} 24 | remote-branch: ${{ env.DOKKU_REMOTE_BRANCH }} 25 | git-push-flags: ${{ env.GIT_PUSH_FLAGS }} 26 | -------------------------------------------------------------------------------- /gmmp/migrations/0005_special_questions_gsheet_integration.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2020-09-07 06:08 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('gmmp', '0004_specialquestions'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='specialquestions', 16 | name='id', 17 | ), 18 | migrations.RemoveField( 19 | model_name='specialquestions', 20 | name='user', 21 | ), 22 | migrations.AddField( 23 | model_name='specialquestions', 24 | name='guid', 25 | field=models.CharField(default=uuid.uuid4, max_length=255, primary_key=True, serialize=False), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /gmmp/static/css/gmmp.css: -------------------------------------------------------------------------------- 1 | /* JET DASHBOARD */ 2 | .dashboard-item-content .submission { 3 | border: 0.07143rem solid #dce0e6; 4 | border-radius: 4px; 5 | } 6 | .dashboard-item-content .submission-action { 7 | border-bottom-left-radius: 4px; 8 | border-bottom-right-radius: 4px; 9 | display: block; 10 | padding: 1.5rem 2rem; 11 | } 12 | .dashboard-item-content .submission-action a { 13 | color: inherit; 14 | } 15 | .dashboard-item-content .submission-description { 16 | padding: 1.5rem 2rem; 17 | height: 13.5rem; 18 | overflow-y: auto; 19 | } 20 | .dashboard-item-content .submission .submission-item { 21 | font-size: 1rem; 22 | padding: 1.45rem 2rem; 23 | } 24 | 25 | .dashboard #footer { 26 | padding: 0 1.42857rem; 27 | } 28 | 29 | /* FORMS */ 30 | .submit-row input.default { 31 | float: right; 32 | margin-right: 0; 33 | } 34 | -------------------------------------------------------------------------------- /reports/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django_countries.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Weights', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 18 | ('country', django_countries.fields.CountryField(max_length=2)), 19 | ('media_type', models.CharField(max_length=32)), 20 | ('weight', models.DecimalField(max_digits=4, decimal_places=2)), 21 | ], 22 | options={ 23 | }, 24 | bases=(models.Model,), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /gmmp/templates/admin/_footer.html: -------------------------------------------------------------------------------- 1 | {% load static i18n %} 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | ## Screenshots 17 | 18 | ## Checklist: 19 | 20 | - [ ] My code follows the style guidelines of this project 21 | - [ ] I have performed a self-review of my own code 22 | - [ ] I have commented my code, particularly in hard-to-understand areas 23 | - [ ] I have made corresponding changes to the documentation 24 | -------------------------------------------------------------------------------- /gmmp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | import django_countries.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Monitor', 18 | fields=[ 19 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 20 | ('country', django_countries.fields.CountryField(max_length=2)), 21 | ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), 22 | ], 23 | options={ 24 | }, 25 | bases=(models.Model,), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /contrib/docker/cmd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python manage.py migrate guardian --noinput 3 | python manage.py migrate jet --noinput 4 | python manage.py migrate dashboard --noinput 5 | python manage.py migrate --noinput # Apply database migrations 6 | django-admin compilemessages # Compile *.po translation files 7 | python manage.py collectstatic --clear --noinput # Collect static files 8 | 9 | # Prepare log files and start outputting logs to stdout 10 | touch /app/logs/gunicorn.log 11 | touch /app/logs/access.log 12 | tail -n 0 -f /app/logs/*.log & 13 | 14 | # Start Gunicorn processes 15 | echo Starting Gunicorn. 16 | exec gunicorn \ 17 | --timeout=${GMMP_GUNICORN_TIMEOUT:-60} \ 18 | --bind=0.0.0.0:8000 \ 19 | --workers=${GMMP_GUNICORN_WORKERS:-3} \ 20 | --worker-class=gevent \ 21 | --log-level=info \ 22 | --log-file=/app/logs/gunicorn.log \ 23 | --access-logfile=/app/logs/access.log \ 24 | --name=gmmp \ 25 | "$@" 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /reports/migrations/0009_delete_tv_media_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.18 on 2021-02-18 12:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def delete_tv_media_type_weights(apps, schema_editor): 7 | Weights = apps.get_model("reports", "Weights") 8 | db_alias = schema_editor.connection.alias 9 | 10 | # Weights with wrong media type 'TV' were accidentally added by 11 | # `import-weights` command. The command has been updated to use 12 | # 'Television' as media type. It is best to delete existing rows 13 | # and import weights afresh. 14 | Weights.objects.using(db_alias).filter(media_type="TV").delete() 15 | 16 | 17 | def backwards(apps, schema_editor): 18 | pass 19 | 20 | 21 | class Migration(migrations.Migration): 22 | 23 | dependencies = [ 24 | ("reports", "0008_gsheetcountryweights"), 25 | ] 26 | 27 | operations = [ 28 | migrations.RunPython( 29 | delete_tv_media_type_weights, 30 | backwards, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /gmmp/dashboard.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext_lazy as _ 2 | from jet.dashboard import modules 3 | from jet.dashboard.dashboard import Dashboard, AppIndexDashboard 4 | 5 | from gmmp.dashboard_modules import ( 6 | AddInternetNewsSubmission, 7 | AddNewspaperSubmission, 8 | AddRadioSubmission, 9 | AddTelevisionSubmission, 10 | AddTwitterSubmission, 11 | Submissions, 12 | ) 13 | 14 | 15 | class CustomIndexDashboard(Dashboard): 16 | columns = 3 17 | 18 | def init_with_context(self, context): 19 | self.available_children.append(modules.LinkList) 20 | self.children.append(AddNewspaperSubmission(column=0, order=0)) 21 | self.children.append(AddRadioSubmission(column=1, order=0)) 22 | self.children.append(AddTelevisionSubmission(column=2, order=0)) 23 | self.children.append(AddInternetNewsSubmission(column=0, order=1)) 24 | self.children.append(AddTwitterSubmission(column=1, order=1)) 25 | self.children.append(Submissions(column=2, order=1)) 26 | -------------------------------------------------------------------------------- /gmmp/management/commands/fix_regions.py: -------------------------------------------------------------------------------- 1 | import os 2 | import csv 3 | from collections import defaultdict 4 | from django.core.management.base import BaseCommand 5 | from forms.models import sheet_models 6 | from forms.modelutils import CountryRegion 7 | 8 | class Command(BaseCommand): 9 | 10 | def handle(self, *args, **options): 11 | file_path = os.path.join(os.path.dirname(os.path.realpath(__file__))) 12 | with open(file_path + "/bad_sheets.csv") as csvfile: 13 | reader = csv.DictReader(csvfile) 14 | sheets_to_fix = defaultdict(list) 15 | for row in reader: 16 | sheets_to_fix[row['sheet_type']].append(row['id']) 17 | for sheet_type, sheet_ids in sheets_to_fix.items(): 18 | model = sheet_models[sheet_type] 19 | for obj_id in sheet_ids: 20 | sheet = model.objects.get(id=obj_id) 21 | sheet.country_region = CountryRegion.objects.get(country=sheet.country) 22 | sheet.save() 23 | self.stdout.write("%s %s" % (sheet_type, obj_id)) 24 | -------------------------------------------------------------------------------- /gmmp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from django.contrib.auth.models import User 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from . import models 7 | 8 | # Define an inline admin descriptor for Employee model 9 | # which acts a bit like a singleton 10 | class MonitorInline(admin.TabularInline): 11 | model = models.Monitor 12 | can_delete = False 13 | verbose_name_plural = _('Monitor Details') 14 | 15 | def monitor_country(obj): 16 | return obj.monitor.country.name 17 | monitor_country.short_description = _('Country') 18 | monitor_country.admin_order_field = 'monitor__country' 19 | 20 | # Define a new User admin 21 | class UserAdmin(UserAdmin): 22 | inlines = (MonitorInline,) 23 | list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', monitor_country) 24 | list_filter = ('monitor__country',) 25 | search_fields = ('username', 'email', 'first_name', 'last_name', 'monitor__country') 26 | 27 | # Re-register UserAdmin 28 | admin.site.unregister(User) 29 | admin.site.register(User, UserAdmin) 30 | -------------------------------------------------------------------------------- /gmmp/migrations/0004_specialquestions.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.15 on 2020-09-01 05:39 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django_countries.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('gmmp', '0003_auto_20200114_1330'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='SpecialQuestions', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('country', django_countries.fields.CountryField(default='KE', max_length=2)), 22 | ('question_1', models.TextField()), 23 | ('question_2', models.TextField()), 24 | ('question_3', models.TextField()), 25 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /forms/migrations/0039_auto_20200120_1536.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.9 on 2020-01-20 15:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('forms', '0038_auto_20200115_0622'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='radioperson', 15 | name='special_qn_1', 16 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(17) Special question 1'), 17 | ), 18 | migrations.AlterField( 19 | model_name='radioperson', 20 | name='special_qn_2', 21 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(18) Special question 2'), 22 | ), 23 | migrations.AlterField( 24 | model_name='radioperson', 25 | name='special_qn_3', 26 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(19) Special question 3'), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /gmmp/migrations/0006_countryuser.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2020-09-28 09:28 2 | 3 | from django.db import migrations, models 4 | import django_countries.fields 5 | import gsheets.mixins 6 | import uuid 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('gmmp', '0005_special_questions_gsheet_integration'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='CountryUser', 18 | fields=[ 19 | ('guid', models.CharField(default=uuid.uuid4, max_length=255, primary_key=True, serialize=False)), 20 | ('country', django_countries.fields.CountryField(default='KE', max_length=2)), 21 | ('firstname', models.CharField(max_length=127)), 22 | ('lastname', models.CharField(max_length=127)), 23 | ('username', models.CharField(max_length=127)), 24 | ('email', models.CharField(max_length=127)), 25 | ('designation', models.CharField(max_length=127)), 26 | ], 27 | bases=(gsheets.mixins.SheetPullableMixin, models.Model), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /forms/migrations/0040_auto_20200120_1538.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.9 on 2020-01-20 15:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('forms', '0039_auto_20200120_1536'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='televisionperson', 15 | name='special_qn_1', 16 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(19) Special question 1'), 17 | ), 18 | migrations.AlterField( 19 | model_name='televisionperson', 20 | name='special_qn_2', 21 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(20) Special question 2'), 22 | ), 23 | migrations.AlterField( 24 | model_name='televisionperson', 25 | name='special_qn_3', 26 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(21) Special question 3'), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /forms/migrations/0041_auto_20200123_1224.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.9 on 2020-01-23 12:24 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('forms', '0040_auto_20200120_1538'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='internetnewsperson', 15 | name='special_qn_1', 16 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(22) Special question 1'), 17 | ), 18 | migrations.AlterField( 19 | model_name='internetnewsperson', 20 | name='special_qn_2', 21 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(23) Special question 2'), 22 | ), 23 | migrations.AlterField( 24 | model_name='internetnewsperson', 25 | name='special_qn_3', 26 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(24) Special question 3'), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## For ubuntu 4 | 5 | ```bash 6 | sudo apt-get install libpq-dev 7 | sudo apt-get install libpython-dev 8 | ``` 9 | 10 | Install a postgres db if you don't already have one 11 | 12 | ```bash 13 | sudo apt-get install postgresql postgresql-contrib 14 | sudo /etc/init.d/postgresql start 15 | ``` 16 | 17 | Install the Heroku toolbelt for deployment, database backup, etc 18 | 19 | ```bash 20 | wget -qO- https://toolbelt.heroku.com/install-ubuntu.sh | sh 21 | ``` 22 | 23 | Grab a backup of the database. 24 | 25 | ```bash 26 | pg_dump > /tmp/dump # You can get the url from the Heroku config. This seems to take a long time 27 | sudo su - postgres 28 | createuser gmmp -W # set password to gmmp 29 | createuser c4saadmin -W # needed to prevent error in dump file 30 | createdb gmmp --owner gmmp 31 | psql U gmmp < /tmp/dump # might get an error complaining the code4saadmin doesn't exist 32 | ``` 33 | 34 | You'll need to install postgres, psycopg as your development user 35 | 36 | ```bash 37 | git clone https://github.com/Code4SA/gmmp.git 38 | cd gmmp 39 | mkdir env 40 | virtualenv env 41 | source env/bin/activate 42 | pip install -r requirements.txt 43 | ``` 44 | -------------------------------------------------------------------------------- /gmmp/templates/admin/submit_line.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/submit_line.html" %} 2 | 3 | {% load static i18n admin_urls %} 4 | 5 |
6 | {% block submit-row %} 7 | {% if show_delete_link and original %} 8 | {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %} 9 | 10 | {% endif %} 11 | {% if show_save_as_new %}{% endif %} 12 | {% if show_save_and_continue %}{% endif %} 13 | {% if show_save_and_add_another %}{% endif %} 14 | {% if show_save %}{% endif %} 15 | {% if show_close %}{% trans 'Close' %}{% endif %} 16 | 17 | {% endblock %} 18 |
19 | -------------------------------------------------------------------------------- /reports/migrations/0003_indonesia-weights.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | from django_countries import countries 6 | 7 | def populate_weights(apps, schema_editor): 8 | Weights = apps.get_model("reports", "Weights") 9 | db_alias = schema_editor.connection.alias 10 | 11 | for item in COUNTRY_WEIGHTS: 12 | country = item['Country'] 13 | item.pop('Country') 14 | for media_type, weight in item.items(): 15 | w = Weights.objects.using(db_alias).create( 16 | country=country, 17 | media_type=media_type, 18 | weight=weight) 19 | w.save() 20 | 21 | def backwards(apps, schema_editor): 22 | pass 23 | 24 | class Migration(migrations.Migration): 25 | 26 | dependencies = [ 27 | ('reports', '0002_populate_weights'), 28 | ] 29 | 30 | operations = [ 31 | migrations.RunPython( 32 | populate_weights, 33 | backwards, 34 | ), 35 | ] 36 | 37 | COUNTRY_WEIGHTS= [ 38 | {'Country': 'ID', 39 | 'Internet': '0', 40 | 'Print': '11', 41 | 'Radio': '1', 42 | 'Television': '7', 43 | 'Twitter': '0'}] 44 | -------------------------------------------------------------------------------- /forms/migrations/0045_auto_20200306_1510.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.9 on 2020-03-06 15:10 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('forms', '0044_auto_20200226_0758'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='radiosheet', 15 | name='num_female_anchors', 16 | field=models.PositiveIntegerField(help_text='The anchor (or announcer, or presenter) is the person who introduces the newscast and the individual items within it. Note: You should only include the anchors/announcers. Do not include reporters or other journalists', verbose_name='Number of female anchors'), 17 | ), 18 | migrations.AlterField( 19 | model_name='televisionsheet', 20 | name='num_female_anchors', 21 | field=models.PositiveIntegerField(help_text='The anchor (or announcer, or presenter) is the person who introduces the newscast and the individual items within it. Note: You should only include the anchors/announcers. Do not include reporters or other journalists', verbose_name='Number of female anchors'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /reports/migrations/0004_transnational_weights.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | from django_countries import countries 6 | 7 | def populate_weights(apps, schema_editor): 8 | Weights = apps.get_model("reports", "Weights") 9 | db_alias = schema_editor.connection.alias 10 | 11 | for item in COUNTRY_WEIGHTS: 12 | country = item['Country'] 13 | item.pop('Country') 14 | for media_type, weight in item.items(): 15 | w = Weights.objects.using(db_alias).create( 16 | country=country, 17 | media_type=media_type, 18 | weight=weight) 19 | w.save() 20 | 21 | def backwards(apps, schema_editor): 22 | pass 23 | 24 | class Migration(migrations.Migration): 25 | 26 | dependencies = [ 27 | ('reports', '0003_indonesia-weights'), 28 | ] 29 | 30 | operations = [ 31 | migrations.RunPython( 32 | populate_weights, 33 | backwards, 34 | ), 35 | ] 36 | 37 | COUNTRY_WEIGHTS= [ 38 | {'Country': 'T1', 39 | 'Internet': '1', 40 | 'Print': '1', 41 | 'Radio': '1', 42 | 'Television': '1', 43 | 'Twitter': '1'}] 44 | -------------------------------------------------------------------------------- /gmmp/management/commands/bulk_password_reset_emails.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django.contrib.auth.models import User 3 | from django.core.mail import send_mass_mail 4 | from django.template.loader import render_to_string 5 | from django.core.mail import get_connection, EmailMultiAlternatives 6 | from django.conf import settings 7 | 8 | 9 | def send_mass_html_mail(subject, message, html_message, from_email, recipient_list): 10 | emails = [] 11 | for recipient in recipient_list: 12 | email = EmailMultiAlternatives(subject, message, from_email, [recipient]) 13 | email.attach_alternative(html_message, 'text/html') 14 | emails.append(email) 15 | return get_connection().send_messages(emails) 16 | 17 | class Command(BaseCommand): 18 | def handle(self, *args, **options): 19 | users = User.objects.filter(last_login=None) 20 | password_reset_form = render_to_string( 21 | 'emails/welcome_password_reset.html', {'SITE_URL': settings.SITE_URL}) 22 | send_mass_html_mail( 23 | 'New account on app.gmmp.ngo', 24 | '''Hi there''', 25 | password_reset_form, 26 | settings.EMAIL_FROM, 27 | [user.email for user in users] 28 | ) 29 | -------------------------------------------------------------------------------- /forms/migrations/0029_auto_20191210_2042.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0028_auto_20191210_2033'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='newspaperperson', 16 | name='special_qn_1', 17 | field=models.CharField(blank=True, max_length=1, verbose_name='(20) Special Question 1', choices=[(b'Y', '(1) Yes'), (b'N', '(2) No')]), 18 | preserve_default=True, 19 | ), 20 | migrations.AlterField( 21 | model_name='newspaperperson', 22 | name='special_qn_2', 23 | field=models.CharField(blank=True, max_length=1, verbose_name='(21) Special Question 2', choices=[(b'Y', '(1) Yes'), (b'N', '(2) No')]), 24 | preserve_default=True, 25 | ), 26 | migrations.AlterField( 27 | model_name='newspaperperson', 28 | name='special_qn_3', 29 | field=models.CharField(blank=True, max_length=1, verbose_name='(22) Special Question 3', choices=[(b'Y', '(1) Yes'), (b'N', '(2) No')]), 30 | preserve_default=True, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /forms/migrations/0030_auto_20191210_2043.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0029_auto_20191210_2042'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='newspaperperson', 16 | name='special_qn_1', 17 | field=models.CharField(blank=True, max_length=1, verbose_name='(20) Special question 1', choices=[(b'Y', '(1) Yes'), (b'N', '(2) No')]), 18 | preserve_default=True, 19 | ), 20 | migrations.AlterField( 21 | model_name='newspaperperson', 22 | name='special_qn_2', 23 | field=models.CharField(blank=True, max_length=1, verbose_name='(21) Special question 2', choices=[(b'Y', '(1) Yes'), (b'N', '(2) No')]), 24 | preserve_default=True, 25 | ), 26 | migrations.AlterField( 27 | model_name='newspaperperson', 28 | name='special_qn_3', 29 | field=models.CharField(blank=True, max_length=1, verbose_name='(22) Special question 3', choices=[(b'Y', '(1) Yes'), (b'N', '(2) No')]), 30 | preserve_default=True, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /forms/migrations/0033_auto_20191212_2003.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0032_auto_20191212_1932'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='radiosheet', 16 | name='num_female_anchors', 17 | field=models.PositiveIntegerField(help_text='The anchor (or announcer, or presenter) is the person who introduces the newscast and the individual items within it. Note: You should only include the anchors/announcers. Do not include reporters or other', verbose_name='Number of female anchors'), 18 | preserve_default=True, 19 | ), 20 | migrations.AlterField( 21 | model_name='televisionsheet', 22 | name='num_female_anchors', 23 | field=models.PositiveIntegerField(help_text='The anchor (or announcer, or presenter) is the person who introduces the newscast and the individual items within it. Note: You should only include the anchors/announcers. Do not include reporters or other', verbose_name='Number of female anchors'), 24 | preserve_default=True, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /reports/migrations/0008_gsheetcountryweights.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.18 on 2021-03-01 21:07 2 | 3 | from django.db import migrations, models 4 | import django_countries.fields 5 | import gsheets.mixins 6 | import uuid 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('reports', '0007_proxy_weights'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='GSheetCountryWeights', 18 | fields=[ 19 | ('guid', models.CharField(default=uuid.uuid4, max_length=255, primary_key=True, serialize=False)), 20 | ('country', django_countries.fields.CountryField(default='KE', max_length=2)), 21 | ('print_weight', models.DecimalField(decimal_places=2, default=0.0, max_digits=4)), 22 | ('radio_weight', models.DecimalField(decimal_places=2, default=0.0, max_digits=4)), 23 | ('tv_weight', models.DecimalField(decimal_places=2, default=0.0, max_digits=4)), 24 | ('internet_weight', models.DecimalField(decimal_places=2, default=0.0, max_digits=4)), 25 | ('twitter_weight', models.DecimalField(decimal_places=2, default=0.0, max_digits=4)), 26 | ], 27 | bases=(gsheets.mixins.SheetPullableMixin, models.Model), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /forms/migrations/0006_auto_20150309_1403.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0005_internetnewsperson_age'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='internetnewsperson', 16 | name='is_photograph', 17 | field=models.PositiveIntegerField(default=None, verbose_name='Is there a photograph of the person in the story?', choices=[(1, 'Yes'), (2, 'No'), (3, 'Do not know')]), 18 | preserve_default=False, 19 | ), 20 | migrations.AddField( 21 | model_name='internetnewsperson', 22 | name='is_quoted', 23 | field=models.CharField(default=None, help_text='

A person is directly quoted if their own words are printed, e.g. "The war against terror is our first priority" said President Bush.

If the story paraphrases what the person said, that is not a direct quote, e.g. President Bush said that top priority would be given to fighting the war against terror.

', max_length=1, verbose_name='Is the person directly quoted', choices=[(b'Y', 'Yes'), (b'N', 'No')]), 24 | preserve_default=False, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /gmmp/templates/report_filter.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block page_content %} 5 | 6 |
7 |
8 |

9 |

Welcome to the 2020 GMMP Reporting tool.

10 |

Select one of the following options to generate the relevant report.

11 |
12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 |
20 |
21 | 22 | {% csrf_token %} 23 | {{ country_form.as_p }} 24 | 25 |
26 |
27 |
28 |
29 |
30 | 31 | {% csrf_token %} 32 | {{ region_form.as_p }} 33 | 34 |
35 |
36 | {% endblock page_content %} 37 | -------------------------------------------------------------------------------- /reports/migrations/0005_duplicate_weights.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.18 on 2021-02-18 12:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def remove_duplicate_weights(apps, schema_editor): 7 | Weights = apps.get_model("reports", "Weights") 8 | db_alias = schema_editor.connection.alias 9 | 10 | # Remove old transnational since a new one will be created with a new code 11 | Weights.objects.using(db_alias).filter(country="T1").delete() 12 | 13 | # Remove all (country, media_type) duplicates 14 | # Based on: https://stackoverflow.com/a/10290420 15 | last_seen_country = None 16 | last_seen_media_type = None 17 | weights = Weights.objects.using(db_alias).all().order_by("country", "media_type") 18 | for weight in weights: 19 | if ( 20 | weight.country == last_seen_country 21 | and weight.media_type == last_seen_media_type 22 | ): 23 | weight.delete() 24 | else: 25 | last_seen_media_type = weight.media_type 26 | last_seen_country = weight.country 27 | 28 | 29 | def backwards(apps, schema_editor): 30 | pass 31 | 32 | 33 | class Migration(migrations.Migration): 34 | 35 | dependencies = [ 36 | ("reports", "0004_transnational_weights"), 37 | ] 38 | 39 | operations = [ 40 | migrations.RunPython( 41 | remove_duplicate_weights, 42 | backwards, 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /forms/migrations/0053_add_monitoring_mode_20200909_1013.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2020-09-10 08:25 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('forms', '0052_make_fields_nullable_20200907_0936'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='internetnewssheet', 15 | name='monitor_mode', 16 | field=models.IntegerField(choices=[(1, 'Long'), (2, 'Short')], default=1), 17 | ), 18 | migrations.AddField( 19 | model_name='newspapersheet', 20 | name='monitor_mode', 21 | field=models.IntegerField(choices=[(1, 'Long'), (2, 'Short')], default=1), 22 | ), 23 | migrations.AddField( 24 | model_name='radiosheet', 25 | name='monitor_mode', 26 | field=models.IntegerField(choices=[(1, 'Long'), (2, 'Short')], default=1), 27 | ), 28 | migrations.AddField( 29 | model_name='televisionsheet', 30 | name='monitor_mode', 31 | field=models.IntegerField(choices=[(1, 'Long'), (2, 'Short')], default=1), 32 | ), 33 | migrations.AddField( 34 | model_name='twittersheet', 35 | name='monitor_mode', 36 | field=models.IntegerField(choices=[(1, 'Long'), (2, 'Short')], default=1), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | db: 4 | image: postgres:9.6 5 | ports: 6 | - "54321:5432" 7 | environment: 8 | - POSTGRES_USER=gmmp 9 | - POSTGRES_PASSWORD=gmmp 10 | - POSTGRES_DB=gmmp 11 | - PGUSER=gmmp 12 | - PGPASSWORD=gmmp 13 | web: 14 | build: . 15 | ports: 16 | - "8000:8000" 17 | volumes: 18 | - .:/app 19 | - logvolume01:/var/log 20 | depends_on: 21 | - db 22 | environment: 23 | - DATABASE_URL=postgresql://gmmp:gmmp@db:5432/gmmp 24 | - DJANGO_SECRET_KEY=somethingsecret 25 | - SITE_URL=http://localhost:8000 26 | - PGHOST=db 27 | - PGDATABASE=gmmp 28 | - PGUSER=gmmp 29 | - PGPASSWORD=gmmp 30 | - PYTHONDONTWRITEBYTECODE=True 31 | - DJANGO_DEBUG=${DJANGO_DEBUG:-True} # For testing deploys 32 | - GSHEETS_WEIGHTS_SPREADSHEET_ID=${GSHEETS_WEIGHTS_SPREADSHEET_ID:-} 33 | - GMMP_EMAIL_HOST_PASSWORD=${GMMP_EMAIL_HOST_PASSWORD:-} 34 | - GMMP_GUNICORN_TIMEOUT=${GMMP_GUNICORN_TIMEOUT:-120} 35 | - GMMP_GUNICORN_WORKERS=${GMMP_GUNICORN_WORKERS:-3} 36 | - GMMP_REPORTS_HISTORICAL_YEAR=${GMMP_REPORTS_HISTORICAL_YEAR:-2010} 37 | - OAUTHLIB_RELAX_TOKEN_SCOPE=1 # https://stackoverflow.com/a/51643134 38 | command: [ 39 | "/cmd.sh", 40 | "--log-level=debug", 41 | "--reload", 42 | "gmmp.wsgi:application", 43 | ] 44 | 45 | volumes: 46 | logvolume01: {} 47 | -------------------------------------------------------------------------------- /gmmp/management/commands/sync_users.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.core.management.base import BaseCommand 3 | from django.contrib.auth.models import User, Group 4 | from django.core.management import call_command 5 | from django.conf import settings 6 | from gmmp.models import Monitor, CountryUser 7 | 8 | class Command(BaseCommand): 9 | def handle(self, *args, **options): 10 | # Remove all users in the system 11 | CountryUser.objects.all().delete() 12 | User.objects.filter().delete() 13 | 14 | # Pull all gmmp users 15 | try: 16 | call_command('syncgsheets') 17 | except Exception: 18 | # TODO check why an exception is being raised despite all users being pulled 19 | pass 20 | 21 | country_users = CountryUser.objects.all() 22 | for country_user in country_users: 23 | # Create users 24 | group, _ = Group.objects.get_or_create(name=country_user.designation) 25 | user, _ = User.objects.get_or_create(email=country_user.email, 26 | first_name=country_user.firstname, last_name=country_user.lastname, username=country_user.username) 27 | user.set_password(settings.COUNTRY_USER_DEFAULT_PASSWORD) 28 | user.groups.add(group) 29 | 30 | user.is_staff = True 31 | user.save() 32 | monitor, _ = Monitor.objects.get_or_create(user=user) 33 | monitor.country = country_user.country 34 | monitor.save() 35 | -------------------------------------------------------------------------------- /gmmp/templatetags/i18n_switcher.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | from django.template.defaultfilters import stringfilter 4 | 5 | register = template.Library() 6 | 7 | 8 | def switch_lang_code(path, language): 9 | 10 | # Get the supported language codes 11 | lang_codes = [c for (c, name) in settings.LANGUAGES] 12 | 13 | # Validate the inputs 14 | if path == "": 15 | raise Exception("URL path for language switch is empty") 16 | elif path[0] != "/": 17 | raise Exception('URL path for language switch does not start with "/"') 18 | elif language not in lang_codes: 19 | raise Exception("%s is not a supported language code" % language) 20 | 21 | # Split the parts of the path 22 | parts = path.split("/") 23 | 24 | # Add or substitute the new language prefix 25 | if parts[1] in lang_codes: 26 | if language == "en": 27 | parts.pop(1) 28 | else: 29 | parts[1] = language 30 | elif not language == "en": 31 | parts[0] = "/" + language 32 | 33 | # Return the full new path 34 | return "/".join(parts) 35 | 36 | 37 | @register.filter 38 | @stringfilter 39 | def switch_i18n_prefix(path, language): 40 | """takes in a string path""" 41 | return switch_lang_code(path, language) 42 | 43 | 44 | @register.filter 45 | def switch_i18n(request, language): 46 | """takes in a request object and gets the path from it""" 47 | return switch_lang_code(request.get_full_path(), language) 48 | -------------------------------------------------------------------------------- /gmmp/static/css/3-col-portfolio.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - 3 Col Portfolio HTML Template (http://startbootstrap.com) 3 | * Code licensed under the Apache License v2.0. 4 | * For details, see http://www.apache.org/licenses/LICENSE-2.0. 5 | */ 6 | 7 | body { 8 | padding-top: 30px; 9 | } 10 | 11 | .portfolio-item { 12 | margin-bottom: 25px; 13 | } 14 | 15 | footer { 16 | margin: 50px 0; 17 | } 18 | 19 | h2 { 20 | color: rgb(51, 122, 183); 21 | } 22 | 23 | .box a, .box a:hover, .box a:active { 24 | text-decoration: none; 25 | } 26 | 27 | .box { 28 | width: 360px; 29 | height: 206px; 30 | font-size: 25px; 31 | background-color: #efefef; 32 | text-align: center; 33 | position: relative; 34 | text-decoration: none; 35 | } 36 | 37 | .box div { 38 | top: 85px; 39 | position:relative; 40 | -webkit-touch-callout: none; 41 | -webkit-user-select: none; 42 | -khtml-user-select: none; 43 | -moz-user-select: none; 44 | -ms-user-select: none; 45 | user-select: none; 46 | color: #454545; 47 | } 48 | 49 | .box i { 50 | top: 120px; 51 | left: 150px; 52 | color: #454545; 53 | } 54 | 55 | .box div:hover, .box i:hover { 56 | cursor: pointer; 57 | text-decoration: none; 58 | } 59 | 60 | .box div:active, .box i:active { 61 | cursor: pointer; 62 | color: #acacac; 63 | } 64 | 65 | .user-greeting img { 66 | margin-bottom: 3px; 67 | margin-left: 3px; 68 | } 69 | 70 | .header-instructions { 71 | color: rgb(51, 122, 183); 72 | font-size: 20px; 73 | } 74 | -------------------------------------------------------------------------------- /gmmp/static/wazimap/js.cba122a9.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["stories.styles.css","barchart.css"],"names":[],"mappings":"AAAA,WACI,qBAAuB,CACvB,WAAY,CACZ,YAAa,CACb,eAAgB,CAChB,gBACJ,CAKA,QACI,aAAc,CACd,WAAY,CACZ,yBAA8B,CAC9B,UAAW,CACX,iBAAkB,CAClB,cAAe,CACf,mBACJ,CAGA,gBAOI,eAAgB,CAIhB,eAAkB,CAClB,QAEJ,CAGA,iCAhBI,qBAAsB,CACtB,cAAe,CACf,cAAe,CACf,UAAW,CACX,aAAc,CACd,oBAAyB,CAEzB,iBAAkB,CAClB,iBAAkB,CAIlB,MAkBJ,CAdA,iBAOI,eAAgB,CAIhB,eAAkB,CAClB,WAEJ,CCrDA,qCACI,YACJ","file":"js.cba122a9.css","sourceRoot":"../src","sourcesContent":[".container {\n border: 1px solid black;\n width: 800px;\n height: 100px;\n margin-top: 50px;\n margin-left: 50px;\n}\n\n.bar {\n}\n\n.d3-tip {\n line-height: 1;\n padding: 8px;\n background: rgba(0, 0, 0, 0.8);\n color: #fff;\n border-radius: 4px;\n font-size: 16px;\n pointer-events: none;\n}\n\n/* Style northward tooltips specifically */\n.d3-tip.n:after {\n box-sizing: border-box;\n display: inline;\n font-size: 10px;\n width: 100%;\n line-height: 1;\n color: rgba(0, 0, 0, 0.8);\n content: \"\\25BC\";\n position: absolute;\n text-align: center;\n\n margin: -2px 0 0 0;\n top: 100%;\n left: 0;\n}\n\n/* Style southward tooltips specifically */\n.d3-tip.s:before {\n box-sizing: border-box;\n display: inline;\n font-size: 10px;\n width: 100%;\n line-height: 1;\n color: rgba(0, 0, 0, 0.8);\n content: \"\\25B2\";\n position: absolute;\n text-align: center;\n\n margin: 0 0 -2px 0;\n bottom: 100%;\n left: 0;\n}\n",".indicator__chart_container rect.bar {\n fill: #E4653D;\n}\n"]} -------------------------------------------------------------------------------- /reports/management/commands/generate_dataset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import csv 3 | from django.core.management.base import BaseCommand 4 | 5 | from reports.report_builder import XLSXReportBuilder 6 | from reports.report_dataset import generate_dataset_desc 7 | 8 | from reports.forms import GlobalForm 9 | 10 | 11 | class Command(BaseCommand): 12 | help = "Generates dataset from the GMMP report" 13 | 14 | def add_arguments(self, parser): 15 | parser.add_argument("-w","--worksheets", nargs="+", help="Worksheets to generate dataset", required=False) 16 | parser.add_argument("-d", "--dataset-details", action="store_true", help="Filename to store the selected worksheet details") 17 | 18 | def handle(self, *args, **options): 19 | # Create the dataset directory if it doesn't exist 20 | os.makedirs("dataset", exist_ok=True) 21 | if options['worksheets']: 22 | dataset_sheets = options['worksheets'] 23 | else: 24 | dataset_sheets = ["ws_05", "ws_06", "ws_09", "ws_15", 25 | "ws_28b", "ws_28c", "ws_30", "ws_38", "ws_41", "ws_47", "ws_48", 26 | "ws_83", "ws_85", "ws_92", "ws_93", "ws_97", "ws_100", "ws_101", 27 | "ws_102", "ws_104"] 28 | 29 | chart_filename = options.get("dataset-details") if options.get("dataset-details") else "gmmp_dataset" 30 | generate_dataset_desc(chart_filename, dataset_sheets) 31 | 32 | form = GlobalForm() 33 | xlsx = XLSXReportBuilder(form).build(dataset_sheets=dataset_sheets) 34 | -------------------------------------------------------------------------------- /forms/migrations/0013_auto_20150324_0700.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0012_auto_20150312_1400'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='internetnewssheet', 16 | name='country', 17 | ), 18 | migrations.RemoveField( 19 | model_name='internetnewssheet', 20 | name='monitor', 21 | ), 22 | migrations.RemoveField( 23 | model_name='newspapersheet', 24 | name='country', 25 | ), 26 | migrations.RemoveField( 27 | model_name='newspapersheet', 28 | name='monitor', 29 | ), 30 | migrations.RemoveField( 31 | model_name='radiosheet', 32 | name='country', 33 | ), 34 | migrations.RemoveField( 35 | model_name='radiosheet', 36 | name='monitor', 37 | ), 38 | migrations.RemoveField( 39 | model_name='televisionsheet', 40 | name='country', 41 | ), 42 | migrations.RemoveField( 43 | model_name='televisionsheet', 44 | name='monitor', 45 | ), 46 | migrations.RemoveField( 47 | model_name='twittersheet', 48 | name='country', 49 | ), 50 | migrations.RemoveField( 51 | model_name='twittersheet', 52 | name='monitor', 53 | ), 54 | ] 55 | -------------------------------------------------------------------------------- /forms/migrations/0012_auto_20150312_1400.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django_countries.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('forms', '0011_auto_20150312_1358'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='internetnewssheet', 17 | name='country', 18 | field=django_countries.fields.CountryField(max_length=2, null=True), 19 | preserve_default=True, 20 | ), 21 | migrations.AlterField( 22 | model_name='newspapersheet', 23 | name='country', 24 | field=django_countries.fields.CountryField(max_length=2, null=True), 25 | preserve_default=True, 26 | ), 27 | migrations.AlterField( 28 | model_name='radiosheet', 29 | name='country', 30 | field=django_countries.fields.CountryField(max_length=2, null=True), 31 | preserve_default=True, 32 | ), 33 | migrations.AlterField( 34 | model_name='televisionsheet', 35 | name='country', 36 | field=django_countries.fields.CountryField(max_length=2, null=True), 37 | preserve_default=True, 38 | ), 39 | migrations.AlterField( 40 | model_name='twittersheet', 41 | name='country', 42 | field=django_countries.fields.CountryField(max_length=2, null=True), 43 | preserve_default=True, 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /forms/migrations/0011_auto_20150312_1358.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django_countries.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('forms', '0010_televisionsheet_comments'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='internetnewssheet', 17 | name='country', 18 | field=django_countries.fields.CountryField(default=None, max_length=2), 19 | preserve_default=False, 20 | ), 21 | migrations.AddField( 22 | model_name='newspapersheet', 23 | name='country', 24 | field=django_countries.fields.CountryField(default=None, max_length=2), 25 | preserve_default=False, 26 | ), 27 | migrations.AddField( 28 | model_name='radiosheet', 29 | name='country', 30 | field=django_countries.fields.CountryField(default=None, max_length=2), 31 | preserve_default=False, 32 | ), 33 | migrations.AddField( 34 | model_name='televisionsheet', 35 | name='country', 36 | field=django_countries.fields.CountryField(default=None, max_length=2), 37 | preserve_default=False, 38 | ), 39 | migrations.AddField( 40 | model_name='twittersheet', 41 | name='country', 42 | field=django_countries.fields.CountryField(default=None, max_length=2), 43 | preserve_default=False, 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /forms/migrations/0057_add_monitor_code.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2020-09-17 11:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('forms', '0056_allow_blank_and_none'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='internetnewssheet', 15 | name='monitor_code', 16 | field=models.CharField(default='', max_length=255, verbose_name='Monitor Code'), 17 | preserve_default=False, 18 | ), 19 | migrations.AddField( 20 | model_name='newspapersheet', 21 | name='monitor_code', 22 | field=models.CharField(default='', max_length=255, verbose_name='Monitor Code'), 23 | preserve_default=False, 24 | ), 25 | migrations.AddField( 26 | model_name='radiosheet', 27 | name='monitor_code', 28 | field=models.CharField(default='', max_length=255, verbose_name='Monitor Code'), 29 | preserve_default=False, 30 | ), 31 | migrations.AddField( 32 | model_name='televisionsheet', 33 | name='monitor_code', 34 | field=models.CharField(default='', max_length=255, verbose_name='Monitor Code'), 35 | preserve_default=False, 36 | ), 37 | migrations.AddField( 38 | model_name='twittersheet', 39 | name='monitor_code', 40 | field=models.CharField(default='', max_length=255, verbose_name='Monitor Code'), 41 | preserve_default=False, 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /forms/migrations/0054_monitoring_mode_messages_20200910_1441.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2020-09-10 14:41 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('forms', '0053_add_monitoring_mode_20200909_1013'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='internetnewssheet', 15 | name='monitor_mode', 16 | field=models.IntegerField(choices=[(1, 'Full monitoring'), (2, 'Short monitoring')], default=1, verbose_name='Format'), 17 | ), 18 | migrations.AlterField( 19 | model_name='newspapersheet', 20 | name='monitor_mode', 21 | field=models.IntegerField(choices=[(1, 'Full monitoring'), (2, 'Short monitoring')], default=1, verbose_name='Format'), 22 | ), 23 | migrations.AlterField( 24 | model_name='radiosheet', 25 | name='monitor_mode', 26 | field=models.IntegerField(choices=[(1, 'Full monitoring'), (2, 'Short monitoring')], default=1, verbose_name='Format'), 27 | ), 28 | migrations.AlterField( 29 | model_name='televisionsheet', 30 | name='monitor_mode', 31 | field=models.IntegerField(choices=[(1, 'Full monitoring'), (2, 'Short monitoring')], default=1, verbose_name='Format'), 32 | ), 33 | migrations.AlterField( 34 | model_name='twittersheet', 35 | name='monitor_mode', 36 | field=models.IntegerField(choices=[(1, 'Full monitoring'), (2, 'Short monitoring')], default=1, verbose_name='Format'), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /forms/migrations/0017_auto_20150331_1815.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0016_auto_20150330_1413'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='radiosheet', 16 | name='item_number', 17 | field=models.PositiveIntegerField(help_text='Write in the number that describes the position of the story within the newscast. E.g. the first story in the newscast is item 1; the seventh story is item 7.', verbose_name='(1) Item Number', choices=[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 10), (11, 11), (12, 12), (13, 13), (14, 14), (15, 15), (16, 16), (17, 17), (18, 18), (19, 19), (20, 20), (21, 21), (22, 22), (23, 23), (24, 24), (25, 25), (26, 26), (27, 27), (28, 28), (29, 29)]), 18 | preserve_default=True, 19 | ), 20 | migrations.AlterField( 21 | model_name='televisionsheet', 22 | name='item_number', 23 | field=models.PositiveIntegerField(help_text='Write in the number that describes the position of the story within the newscast. E.g. the first story in the newscast is item 1; the seventh story is item 7.', verbose_name='(1) Item Number', choices=[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 10), (11, 11), (12, 12), (13, 13), (14, 14), (15, 15), (16, 16), (17, 17), (18, 18), (19, 19), (20, 20), (21, 21), (22, 22), (23, 23), (24, 24), (25, 25), (26, 26), (27, 27), (28, 28), (29, 29)]), 24 | preserve_default=True, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /forms/migrations/0060_add_moldova_to_country_region.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | COUNTRY_REGION = [ 7 | ("Moldova", "Europe"), 8 | ] 9 | 10 | def populate_country_region(apps, schema_editor): 11 | from django_countries import countries 12 | CountryRegion = apps.get_model("forms", "CountryRegion") 13 | db_alias = schema_editor.connection.alias 14 | 15 | country_region_objs = CountryRegion.objects.using(db_alias).all() 16 | region_map = {} 17 | 18 | # Map country codes to regions 19 | for country_region in COUNTRY_REGION: 20 | code = countries.by_name(country_region[0]) 21 | if code: 22 | if country_region[1] in region_map: 23 | region_map[country_region[1]].append(code) 24 | else: 25 | region_map[country_region[1]] = [code] 26 | 27 | # Create CountryRegion objects for supplied pairs 28 | for region, country_list in region_map.items(): 29 | for country in country_list: 30 | # Is this check necessary? 31 | if not country_region_objs.filter(country=country): 32 | CountryRegion.objects.using(db_alias).create( 33 | country=country, 34 | region=region) 35 | 36 | 37 | def backwards(apps, schema_editor): 38 | """ 39 | Table gets dropped, so no need to delete the rows 40 | """ 41 | pass 42 | 43 | 44 | class Migration(migrations.Migration): 45 | 46 | dependencies = [ 47 | ("forms", "0059_update_country_region"), 48 | ] 49 | 50 | operations = [ 51 | migrations.RunPython(populate_country_region, backwards), 52 | ] 53 | -------------------------------------------------------------------------------- /gmmp/management/commands/map_weights_to_codes.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from pprint import pprint 3 | 4 | from django.core.management.base import BaseCommand 5 | 6 | from django_countries import countries 7 | from forms.modelutils import CountryRegion 8 | 9 | class Command(BaseCommand): 10 | args = 'input_file output_file' 11 | help = 'Maps the given country names to there codes and regions.' 12 | def handle(self, *args, **options): 13 | country_weightings = {} 14 | with open(args[1], 'wb') as output: 15 | with open(args[0]) as csvfile: 16 | writer = csv.writer(output) 17 | reader = csv.DictReader(csvfile) 18 | writer.writerow(['Country', 'Region', 'Print', 'Radio', 'TV', 'Online']) 19 | for row in reader: 20 | if row['Country'] == "Ivory Coast": 21 | row['Country'] = u"C\xf4te d'Ivoire" 22 | code = countries.by_name(row['Country']) 23 | region = CountryRegion.objects.get(country=code).region 24 | if not code: 25 | self.stdout.write('Country not found %s' % row['Country']) 26 | break 27 | writer.writerow([ 28 | code, region, row['Print'], row['Radio'], row['TV'], row['Online'] 29 | ]) 30 | country_weightings[code] = { 31 | 'Region': region, 32 | 'Print': row['Print'], 33 | 'Radio': row['Radio'], 34 | 'Television': row['TV'], 35 | 'Internet': row['Online'] 36 | } 37 | pprint(country_weightings) 38 | -------------------------------------------------------------------------------- /gmmp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | from django.contrib.auth.models import User 4 | from django_countries.fields import CountryField 5 | from gsheets.mixins import SheetPullableMixin 6 | from uuid import uuid4 7 | 8 | class Monitor(models.Model): 9 | user = models.OneToOneField(User, on_delete=models.CASCADE) 10 | country = CountryField(default='KE') 11 | 12 | def __unicode__(self): 13 | return "%s" % self.country 14 | 15 | class SpecialQuestions(SheetPullableMixin, models.Model): 16 | spreadsheet_id = settings.GSHEETS_SPECIAL_QUESTIONS['SPREADSHEET_ID'] 17 | sheet_name = settings.GSHEETS_SPECIAL_QUESTIONS['SHEET_NAME'] 18 | model_id_field = 'guid' 19 | sheet_id_field = 'Platform ID' 20 | 21 | guid = models.CharField(primary_key=True, max_length=255, default=uuid4) 22 | 23 | country = CountryField(default='KE') 24 | question_1 = models.TextField() 25 | question_2 = models.TextField() 26 | question_3 = models.TextField() 27 | 28 | def __str__(self): 29 | return f"{self.country} Special Questions" 30 | 31 | class CountryUser(SheetPullableMixin, models.Model): 32 | spreadsheet_id = settings.GSHEET_COUNTRY_USERS['SPREADSHEET_ID'] 33 | sheet_name = settings.GSHEET_COUNTRY_USERS['SHEET_NAME'] 34 | model_id_field = 'guid' 35 | sheet_id_field = 'Platform ID' 36 | 37 | guid = models.CharField(primary_key=True, max_length=255, default=uuid4) 38 | 39 | country = CountryField(default="KE") 40 | firstname = models.CharField(max_length=127) 41 | lastname = models.CharField(max_length=127) 42 | username = models.CharField(max_length=127) 43 | email = models.CharField(max_length=127) 44 | designation = models.CharField(max_length=127) 45 | -------------------------------------------------------------------------------- /forms/migrations/0036_auto_20200114_1337.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.9 on 2020-01-14 13:37 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('forms', '0035_auto_20200114_1330'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='internetnewssheet', 15 | name='stereotypes', 16 | field=models.PositiveIntegerField(choices=[(1, '(1) Agree'), (2, '(2) Disagree')], verbose_name='(9) This story clearly challenges gender stereotypes'), 17 | ), 18 | migrations.AlterField( 19 | model_name='newspapersheet', 20 | name='stereotypes', 21 | field=models.PositiveIntegerField(choices=[(1, '(1) Agree'), (2, '(2) Disagree')], verbose_name='(8) This story clearly challenges gender stereotypes'), 22 | ), 23 | migrations.AlterField( 24 | model_name='radiosheet', 25 | name='stereotypes', 26 | field=models.PositiveIntegerField(choices=[(1, '(1) Agree'), (2, '(2) Disagree')], verbose_name='(7) This story clearly challenges gender stereotypes'), 27 | ), 28 | migrations.AlterField( 29 | model_name='televisionsheet', 30 | name='stereotypes', 31 | field=models.PositiveIntegerField(choices=[(1, '(1) Agree'), (2, '(2) Disagree')], verbose_name='(7) This story clearly challenges gender stereotypes'), 32 | ), 33 | migrations.AlterField( 34 | model_name='twittersheet', 35 | name='stereotypes', 36 | field=models.PositiveIntegerField(choices=[(1, '(1) Agree'), (2, '(2) Disagree')], verbose_name='(6) This story clearly challenges gender stereotypes'), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /gmmp/static/wazimap/normalize.83e55e48.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:none}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} 2 | /*# sourceMappingURL=normalize.83e55e48.css.map */ -------------------------------------------------------------------------------- /forms/migrations/0022_assign_country_region_to_sheet_models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | def assign_country_region_to_sheet(apps, schema_editor): 7 | from forms.models import sheet_models 8 | 9 | CountryRegion = apps.get_model("forms", "CountryRegion") 10 | db_alias = schema_editor.connection.alias 11 | 12 | for name, model in sheet_models.items(): 13 | sheets_model = apps.get_model("forms", model._meta.object_name) 14 | sheets = sheets_model.objects.using(db_alias).all() 15 | for sheet in sheets: 16 | try: 17 | country_region = CountryRegion.objects.using(db_alias).get(country=sheet.country.code) 18 | sheet.country_region = country_region 19 | sheet.save() 20 | except CountryRegion.DoesNotExist: 21 | # Assign to unmapped CountryRegion object 22 | country_region = CountryRegion.objects.using(db_alias).get(region='Unmapped') 23 | sheet.country_region = country_region 24 | sheet.save() 25 | 26 | def backwards(apps, schema_editor): 27 | from forms.models import sheet_models 28 | db_alias = schema_editor.connection.alias 29 | 30 | for name, model in sheet_models.items(): 31 | sheets_model = apps.get_model("forms", model._meta.object_name) 32 | sheets = sheets_model.objects.using(db_alias).all() 33 | for sheet in sheets: 34 | sheet.country_region = None 35 | sheet.save() 36 | 37 | class Migration(migrations.Migration): 38 | 39 | dependencies = [ 40 | ('forms', '0021_auto_20150511_1414'), 41 | ] 42 | 43 | operations = [ 44 | migrations.RunPython( 45 | assign_country_region_to_sheet, 46 | backwards, 47 | ), 48 | ] 49 | -------------------------------------------------------------------------------- /gmmp/signals.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ObjectDoesNotExist 2 | from django.db.models.signals import pre_save, post_save 3 | from django.dispatch import receiver 4 | from django.contrib.auth.models import User, Group 5 | from django_countries.fields import countries 6 | from gsheets.signals import sheet_row_processed 7 | from .models import Monitor, SpecialQuestions, CountryUser 8 | 9 | @receiver(post_save, sender=User) 10 | def create_admin_group(sender, instance, signal, created, **kwargs): 11 | try: 12 | monitor = instance.monitor 13 | except: 14 | monitor, created = Monitor.objects.get_or_create(user=instance) 15 | monitor.save() 16 | Group.objects.get_or_create(name='%s_admin' % monitor.country) 17 | 18 | @receiver(sheet_row_processed, sender=SpecialQuestions) 19 | def match_special_questions_columns_to_fields(instance=None, created=None, row_data=None, **kwargs): 20 | try: 21 | instance.country = countries.alpha2(row_data['Country']) 22 | instance.question_1 = row_data['Question 1'].strip() 23 | instance.question_2 = row_data['Question 2'].strip() 24 | instance.question_3 = row_data['Question 3'].strip() 25 | instance.save() 26 | except (ObjectDoesNotExist, KeyError): 27 | pass 28 | 29 | @receiver(sheet_row_processed, sender=CountryUser) 30 | def match_country_users_columns_to_fields(instance=None, created=None, row_data=None, **kwargs): 31 | try: 32 | instance.country = countries.alpha2(row_data['Country']) 33 | instance.firstname = row_data['Firstname'].strip() 34 | instance.lastname = row_data['Lastname'].strip() 35 | instance.username = row_data['Username'].strip() 36 | instance.email = row_data['Email'].strip() 37 | instance.designation = row_data['Designation'].strip() 38 | instance.save() 39 | except (ObjectDoesNotExist, KeyError): 40 | pass 41 | -------------------------------------------------------------------------------- /reports/signals.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ObjectDoesNotExist 2 | from django.dispatch import receiver 3 | 4 | from django_countries import countries 5 | from gsheets.signals import sheet_row_processed 6 | 7 | from .models import Weights, GSheetCountryWeights 8 | 9 | 10 | @receiver(sheet_row_processed, sender=GSheetCountryWeights) 11 | def update_or_create_weights_from_gsheetweights( 12 | instance=None, created=None, row_data=None, **kwargs 13 | ): 14 | try: 15 | country = countries.alpha2(row_data["Country"]) 16 | print_weight = row_data["Print"] 17 | radio_weight = row_data["Radio"] 18 | television_weight = row_data["Television"] 19 | internet_weight = row_data["Internet"] 20 | twitter_weight = row_data["Twitter"] 21 | Weights.objects.update_or_create( 22 | country=country, media_type="Print", defaults={"weight": print_weight} 23 | ) 24 | Weights.objects.update_or_create( 25 | country=country, media_type="Radio", defaults={"weight": radio_weight} 26 | ) 27 | Weights.objects.update_or_create( 28 | country=country, 29 | media_type="Television", 30 | defaults={"weight": television_weight}, 31 | ) 32 | Weights.objects.update_or_create( 33 | country=country, media_type="Internet", defaults={"weight": internet_weight} 34 | ) 35 | Weights.objects.update_or_create( 36 | country=country, media_type="Twitter", defaults={"weight": twitter_weight} 37 | ) 38 | instance.country = country 39 | instance.print_weight = print_weight 40 | instance.radio_weight = radio_weight 41 | instance.tv_weight = television_weight 42 | instance.internet_weight = internet_weight 43 | instance.twitter_weight = twitter_weight 44 | instance.save() 45 | except (ObjectDoesNotExist, KeyError): 46 | pass 47 | -------------------------------------------------------------------------------- /reports/models.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | from django.conf import settings 4 | from django.db import models 5 | 6 | from django_countries.fields import CountryField 7 | from gsheets.mixins import SheetPullableMixin 8 | 9 | 10 | class Weights(models.Model): 11 | country = CountryField() 12 | media_type = models.CharField(max_length=32) 13 | weight = models.DecimalField(max_digits=4, decimal_places=2) 14 | 15 | def __str__(self): 16 | return f"{self.country} {self.media_type} {self.weight}" 17 | 18 | class Meta: 19 | constraints = [ 20 | models.UniqueConstraint( 21 | fields=["country", "media_type"], name="country_media_type_key" 22 | ) 23 | ] 24 | 25 | 26 | class GSheetCountryWeights(SheetPullableMixin, models.Model): 27 | spreadsheet_id = settings.GSHEETS_WEIGHTS["SPREADSHEET_ID"] 28 | sheet_name = settings.GSHEETS_WEIGHTS["GLOBAL_WEIGHTS_SHEET_NAME"] 29 | model_id_field = "guid" 30 | sheet_id_field = "Platform ID" 31 | 32 | guid = models.CharField(primary_key=True, max_length=255, default=uuid4) 33 | 34 | # NOTE(kilemensi): gsheets performs default insert for new rows before 35 | # doing an update with correct data. This means the model 36 | # must have valid default values for all fields. 37 | country = CountryField(default="KE") 38 | print_weight = models.DecimalField(max_digits=4, decimal_places=2, default=0.0) 39 | radio_weight = models.DecimalField(max_digits=4, decimal_places=2, default=0.0) 40 | tv_weight = models.DecimalField(max_digits=4, decimal_places=2, default=0.0) 41 | internet_weight = models.DecimalField(max_digits=4, decimal_places=2, default=0.0) 42 | twitter_weight = models.DecimalField(max_digits=4, decimal_places=2, default=0.0) 43 | 44 | def __str__(self): 45 | return f"{self.country} [{self.print_weight}, {self.radio_weight}, {self.tv_weight}, {self.internet_weight}, {self.twitter_weight}]" 46 | -------------------------------------------------------------------------------- /reports/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from reports.report_details import get_countries, get_regions 3 | 4 | 5 | class GlobalForm(forms.Form): 6 | pass 7 | 8 | class CountryForm(forms.Form): 9 | """ 10 | Due to the system checks added in django 1.9 https://docs.djangoproject.com/en/3.1/releases/1.9/#urls, 11 | this form gets imported automatically when the url imports happen. This in turn causes the COUNTRIES = get_countries() 12 | variable to be called causing database query errors since migrations haven't been applied yet. 13 | Removing COUNTRIES = get_countries() and replacing it with get_form_countries ensures that the get_countries function 14 | will now only be explicitly called as opposed to being automatically called. 15 | """ 16 | # Only show countries for which data has been submitted 17 | 18 | country = forms.ChoiceField( 19 | label='Country', 20 | choices=get_countries) 21 | 22 | def get_form_countries(self): 23 | return get_countries() 24 | 25 | def filter_countries(self): 26 | if self.cleaned_data['country'] == 'ALL': 27 | return get_countries() 28 | else: 29 | return [(code, country) for code, country in get_countries() if code == self.cleaned_data['country']] 30 | 31 | class RegionForm(forms.Form): 32 | """ 33 | Due to the system checks added in django 1.9 https://docs.djangoproject.com/en/3.1/releases/1.9/#urls, 34 | this form gets imported automatically when the url imports happen. This in turn causes the REGIONS = get_regions() 35 | variable to be called causing database query errors since migrations haven't been applied yet. 36 | Removing REGIONS = get_regions() and replacing it with get_form_regions ensures that the get_regions function 37 | will now only be explicitly called as opposed to being automatically called. 38 | """ 39 | 40 | region = forms.ChoiceField( 41 | label='Region', 42 | choices=get_regions) 43 | 44 | def get_form_regions(self): 45 | return get_regions() 46 | 47 | -------------------------------------------------------------------------------- /forms/migrations/0044_auto_20200226_0758.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.9 on 2020-02-26 07:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('forms', '0043_update_country_region'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='televisionsheet', 15 | old_name='television_channel', 16 | new_name='channel', 17 | ), 18 | migrations.RemoveField( 19 | model_name='televisionsheet', 20 | name='station_name', 21 | ), 22 | migrations.AlterField( 23 | model_name='internetnewssheet', 24 | name='shared_on_facebook', 25 | field=models.CharField(choices=[('Y', '(1) Yes'), ('N', '(2) No')], help_text="Has this story been shared by the media house on its Facebook Page?\n\n
Scroll down the media house's Facebook page to check.", max_length=1, verbose_name='(5) Shared on Facebook'), 26 | ), 27 | migrations.AlterField( 28 | model_name='internetnewssheet', 29 | name='shared_via_twitter', 30 | field=models.CharField(choices=[('Y', '(1) Yes'), ('N', '(2) No')], help_text='Has this story been shared by the media house via Twitter?\n\n
Enter the exact URL of the story into https://twitter.com - answer yes if the media house\'s name appears in the search results.', max_length=1, verbose_name='(4) Shared via twitter?'), 31 | ), 32 | migrations.AlterField( 33 | model_name='radiosheet', 34 | name='station_name', 35 | field=models.CharField(help_text="Be as specific as possible. E.g. if the radio company is called RRI, and if the newscast is broadcast on its third channel, write in 'RRI-3'.", max_length=255, verbose_name='Channel'), 36 | ), 37 | migrations.RenameField( 38 | model_name='radiosheet', 39 | old_name='station_name', 40 | new_name='channel', 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /gmmp/urls.py: -------------------------------------------------------------------------------- 1 | import debug_toolbar 2 | from django.conf import settings 3 | from django.conf.urls.i18n import i18n_patterns 4 | from django.conf.urls.static import static 5 | from django.contrib import admin 6 | from django.contrib.auth import views as auth_views 7 | from django.urls import include, path, re_path 8 | from django.views.generic.base import RedirectView 9 | 10 | from gmmp import settings 11 | from gmmp.views import CustomPassowrdResetView, CustomPasswordResetDoneView 12 | from reports.views import WazimapView 13 | 14 | admin.site.site_header = settings.ADMIN_SITE_SITE_HEADER 15 | admin.site.site_title = settings.ADMIN_SITE_SITE_TITLE 16 | admin.site.site_url = settings.ADMIN_SITE_SITE_URL 17 | admin.site.index_title = settings.ADMIN_SITE_INDEX_TITLE 18 | 19 | urlpatterns = ( 20 | i18n_patterns( 21 | # Django JET URLS 22 | re_path(r"^jet/", include("jet.urls", "jet")), 23 | # Django JET dashboard URLS 24 | re_path(r"^jet/dashboard/", include("jet.dashboard.urls", "jet-dashboard")), 25 | path( 26 | "admin/password_reset/", 27 | CustomPassowrdResetView.as_view(), 28 | name="admin_password_reset", 29 | ), 30 | path( 31 | "admin/password_reset/done/", 32 | CustomPasswordResetDoneView.as_view(), 33 | name="password_reset_done", 34 | ), 35 | path( 36 | "reset///", 37 | auth_views.PasswordResetConfirmView.as_view(), 38 | name="password_reset_confirm", 39 | ), 40 | path( 41 | "reset/done/", 42 | auth_views.PasswordResetCompleteView.as_view(), 43 | name="password_reset_complete", 44 | ), 45 | # Admin site URLS 46 | path("admin/", admin.site.urls), 47 | re_path(r"^$", RedirectView.as_view(url="/admin"), name="go-to-admin"), 48 | path('reports/', include('reports.urls')), 49 | path('genmap', WazimapView.as_view(), name='genmap'), 50 | prefix_default_language=False, 51 | ) 52 | + [path('__debug__/', include(debug_toolbar.urls))] 53 | + [path("", include("gsheets.urls"))] 54 | + [path("i18n/", include("django.conf.urls.i18n"))] 55 | + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 56 | ) 57 | -------------------------------------------------------------------------------- /gmmp/templates/admin/login.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n admin_static %} 3 | 4 | {% block extrastyle %} 5 | {{ block.super }} 6 | 7 | {% endblock %} 8 | 9 | {% block bodyclass %}{{ block.super }} login{% endblock %} 10 | 11 | {% block usertools %}{% endblock %} 12 | 13 | {% block nav-global %}{% endblock %} 14 | 15 | {% block content_title %}{% endblock %} 16 | 17 | {% block breadcrumbs %}{% endblock %} 18 | 19 | 20 | {% block content %} 21 | {% if form.errors and not form.non_field_errors %} 22 |

23 | {% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} 24 |

25 | {% endif %} 26 | 27 | {% if form.non_field_errors %} 28 | {% for error in form.non_field_errors %} 29 |

30 | {{ error }} 31 |

32 | {% endfor %} 33 | {% endif %} 34 | 35 |
36 | {% csrf_token %} 37 | 38 |
39 | {{ form.username.errors }} 40 | {{ form.username.label_tag }} {{ form.username }} 41 |
42 |
43 | {{ form.password.errors }} 44 | {{ form.password.label_tag }} {{ form.password }} 45 | 46 |
47 | {% url 'admin_password_reset' as password_reset_url %} 48 | {% if password_reset_url %} 49 | 52 | {% endif %} 53 |
54 | 55 |
56 | 57 | 58 | 59 | 62 |
63 | {% endblock %} 64 | 65 | {% block footer %} 66 | {% endblock %} 67 | -------------------------------------------------------------------------------- /forms/migrations/0025_auto_20191210_1924.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0024_auto_20191210_1805'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='newspapersheet', 16 | name='person_secondary', 17 | ), 18 | migrations.AlterField( 19 | model_name='newspapersheet', 20 | name='comments', 21 | field=models.TextField(verbose_name='(23) Describe any photographs included in the story and the conclusions you draw from them.', blank=True), 22 | preserve_default=True, 23 | ), 24 | migrations.AlterField( 25 | model_name='newspapersheet', 26 | name='further_analysis', 27 | field=models.CharField(help_text="

A story warrants further analysis if it clearly perpetuates or clearly challenges gender stereotypes, if it includes women's opinions in a remarkable way, if it contributes to an understanding of inequalities between women and men, if it mentions or calls attention to women's human rights, etc. Consult the guide for further explanation", max_length=1, verbose_name='(24) Does this story warrant further analysis?', choices=[(b'Y', '(1) Yes'), (b'N', '(2) No')]), 28 | preserve_default=True, 29 | ), 30 | migrations.AlterField( 31 | model_name='newspapersheet', 32 | name='inequality_women', 33 | field=models.PositiveIntegerField(verbose_name='(7) This story clearly highlights issues of inequality between women and men', choices=[(1, '(1) Agree'), (2, '(2) Disagree'), (3, '(3) Neither agree nor disagree'), (4, '(4) Do not know')]), 34 | preserve_default=True, 35 | ), 36 | migrations.AlterField( 37 | model_name='newspapersheet', 38 | name='stereotypes', 39 | field=models.PositiveIntegerField(help_text='This story clearly challenges gender stereotypes', verbose_name='(8) Challenges Stereotypes', choices=[(1, '(1) Agree'), (2, '(2) Disagree'), (3, '(3) Neither agree nor disagree'), (4, '(4) Do not know')]), 40 | preserve_default=True, 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /forms/migrations/0002_auto_20150309_1227.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='internetnewssheet', 16 | name='equality_rights', 17 | field=models.CharField(help_text="Scan the full news story and code 'Yes' if it quotes or makes reference to any piece of legislation or policy that promotes gender equality or human rights.", max_length=1, verbose_name='Reference to gender equality / human rights legislation/ policy', choices=[(b'Y', 'Yes'), (b'N', 'No')]), 18 | preserve_default=True, 19 | ), 20 | migrations.AlterField( 21 | model_name='newspapersheet', 22 | name='equality_rights', 23 | field=models.CharField(help_text="Scan the full news story and code 'Yes' if it quotes or makes reference to any piece of legislation or policy that promotes gender equality or human rights.", max_length=1, verbose_name='Reference to gender equality / human rights legislation/ policy', choices=[(b'Y', 'Yes'), (b'N', 'No')]), 24 | preserve_default=True, 25 | ), 26 | migrations.AlterField( 27 | model_name='radiosheet', 28 | name='equality_rights', 29 | field=models.CharField(help_text="Scan the full news story and code 'Yes' if it quotes or makes reference to any piece of legislation or policy that promotes gender equality or human rights.", max_length=1, verbose_name='Reference to gender equality / human rights legislation/ policy', choices=[(b'Y', 'Yes'), (b'N', 'No')]), 30 | preserve_default=True, 31 | ), 32 | migrations.AlterField( 33 | model_name='televisionsheet', 34 | name='equality_rights', 35 | field=models.CharField(help_text="Scan the full news story and code 'Yes' if it quotes or makes reference to any piece of legislation or policy that promotes gender equality or human rights.", max_length=1, verbose_name='Reference to gender equality / human rights legislation/ policy', choices=[(b'Y', 'Yes'), (b'N', 'No')]), 36 | preserve_default=True, 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /forms/migrations/0009_auto_20150312_1347.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0008_auto_20150309_1902'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='internetnewssheet', 16 | name='comments', 17 | ), 18 | migrations.AddField( 19 | model_name='radiosheet', 20 | name='comments', 21 | field=models.TextField(verbose_name='Describe any photographs included in the story and the conclusions you draw from them.', blank=True), 22 | preserve_default=True, 23 | ), 24 | migrations.AlterField( 25 | model_name='internetnewsperson', 26 | name='family_role', 27 | field=models.CharField(help_text="Code yes only if the word 'wife', 'husband' etc is actually used to describe the person.", max_length=1, verbose_name='Family Role Given?', choices=[(b'Y', 'Yes'), (b'N', 'No')]), 28 | preserve_default=True, 29 | ), 30 | migrations.AlterField( 31 | model_name='newspaperperson', 32 | name='family_role', 33 | field=models.CharField(help_text="Code yes only if the word 'wife', 'husband' etc is actually used to describe the person.", max_length=1, verbose_name='Family Role Given?', choices=[(b'Y', 'Yes'), (b'N', 'No')]), 34 | preserve_default=True, 35 | ), 36 | migrations.AlterField( 37 | model_name='radioperson', 38 | name='family_role', 39 | field=models.CharField(help_text="Code yes only if the word 'wife', 'husband' etc is actually used to describe the person.", max_length=1, verbose_name='Family Role Given?', choices=[(b'Y', 'Yes'), (b'N', 'No')]), 40 | preserve_default=True, 41 | ), 42 | migrations.AlterField( 43 | model_name='televisionperson', 44 | name='family_role', 45 | field=models.CharField(help_text="Code yes only if the word 'wife', 'husband' etc is actually used to describe the person.", max_length=1, verbose_name='Family Role Given?', choices=[(b'Y', 'Yes'), (b'N', 'No')]), 46 | preserve_default=True, 47 | ), 48 | ] 49 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Multi-stage build 2 | 3 | ############################################################################### 4 | ## Python base image 5 | ############################################################################### 6 | FROM python:3.8-slim AS python-base 7 | 8 | ### Arg 9 | #### See: https://stackoverflow.com/a/56569081 10 | ARG DEBIAN_FRONTEND=noninteractive 11 | 12 | ### Env 13 | ENV APP_HOST=. 14 | ENV APP_DOCKER=/app 15 | ENV PYTHONDONTWRITEBYTECODE 1 16 | ENV PYTHONUNBUFFERED 1 17 | 18 | ### Dependencies 19 | #### System 20 | #### We need libpq-dev in both build and final runtime image 21 | RUN apt-get update \ 22 | && apt-get -y upgrade \ 23 | && apt-get install gettext libpq-dev --no-install-recommends -y \ 24 | && apt-get clean 25 | 26 | ############################################################################### 27 | ## Python builder base image 28 | ############################################################################### 29 | FROM python-base AS python-builder-base 30 | 31 | ### Dependencies 32 | #### System 33 | 34 | RUN apt-get install gcc python-dev --no-install-recommends -y \ 35 | && apt-get clean \ 36 | && pip install --upgrade pip 37 | 38 | ############################################################################### 39 | ## Python builder image 40 | ############################################################################### 41 | 42 | FROM python-builder-base AS python-builder 43 | 44 | ### Env 45 | ENV PATH=/root/.local/bin:$PATH 46 | 47 | ### Dependencies 48 | #### Python 49 | COPY ${APP_HOST}/requirements-all.txt ${APP_HOST}/requirements.txt /tmp/ 50 | RUN pip install --user -r /tmp/requirements.txt 51 | 52 | ############################################################################### 53 | ## App image 54 | ############################################################################### 55 | FROM python-base AS app 56 | 57 | ### Env 58 | ENV PATH=/root/.local/bin:$PATH 59 | 60 | ### Dependencies 61 | #### Python (copy from python-builder) 62 | COPY --from=python-builder /root/.local /root/.local 63 | 64 | ### Volumes 65 | WORKDIR ${APP_DOCKER} 66 | RUN mkdir media static logs 67 | VOLUME ["${APP_DOCKER}/media/", "${APP_DOCKER}/logs/"] 68 | 69 | # Expose server port 70 | EXPOSE 8000 71 | 72 | ### Setup app 73 | COPY ${APP_HOST} ${APP_DOCKER} 74 | COPY ${APP_HOST}/contrib/docker/*.sh / 75 | RUN chmod +x /entrypoint.sh \ 76 | && chmod +x /cmd.sh \ 77 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 78 | 79 | ### Run app 80 | ENTRYPOINT ["/entrypoint.sh"] 81 | CMD ["/cmd.sh", "gmmp.wsgi:application"] 82 | -------------------------------------------------------------------------------- /forms/migrations/0050_add_created_and_updated_at.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.15 on 2020-08-26 06:23 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0049_auto_20200820_0739'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='internetnewssheet', 16 | name='created_at', 17 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 18 | preserve_default=False, 19 | ), 20 | migrations.AddField( 21 | model_name='internetnewssheet', 22 | name='updated_at', 23 | field=models.DateTimeField(auto_now=True), 24 | ), 25 | migrations.AddField( 26 | model_name='newspapersheet', 27 | name='created_at', 28 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 29 | preserve_default=False, 30 | ), 31 | migrations.AddField( 32 | model_name='newspapersheet', 33 | name='updated_at', 34 | field=models.DateTimeField(auto_now=True), 35 | ), 36 | migrations.AddField( 37 | model_name='radiosheet', 38 | name='created_at', 39 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 40 | preserve_default=False, 41 | ), 42 | migrations.AddField( 43 | model_name='radiosheet', 44 | name='updated_at', 45 | field=models.DateTimeField(auto_now=True), 46 | ), 47 | migrations.AddField( 48 | model_name='televisionsheet', 49 | name='created_at', 50 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 51 | preserve_default=False, 52 | ), 53 | migrations.AddField( 54 | model_name='televisionsheet', 55 | name='updated_at', 56 | field=models.DateTimeField(auto_now=True), 57 | ), 58 | migrations.AddField( 59 | model_name='twittersheet', 60 | name='created_at', 61 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 62 | preserve_default=False, 63 | ), 64 | migrations.AddField( 65 | model_name='twittersheet', 66 | name='updated_at', 67 | field=models.DateTimeField(auto_now=True), 68 | ), 69 | ] 70 | -------------------------------------------------------------------------------- /static/js/tabs.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | var set_button_navigation = function() { 3 | var last_tab = $('.changeform-tabs li:last-child').hasClass('selected'); 4 | if(last_tab) { 5 | $('input[name=_addanother]').show(); 6 | $('input[name=_save]').show(); 7 | $('input[name=_continue]').hide(); 8 | }else{ 9 | $('input[name=_addanother]').hide(); 10 | $('input[name=_save]').hide(); 11 | $('input[name=_continue]').show(); 12 | } 13 | }; 14 | 15 | $(document).ready(function() { 16 | var newspapersheet_form = $('#newspapersheet_form').length > 0; 17 | var radiosheet_form = $('#radiosheet_form').length > 0; 18 | var televisionsheet_form = $('#televisionsheet_form').length > 0; 19 | var internetnewssheet_form = $('#internetnewssheet_form').length > 0; 20 | var twittersheet_form = $('#twittersheet_form').length > 0; 21 | 22 | if(newspapersheet_form || twittersheet_form || radiosheet_form || televisionsheet_form || internetnewssheet_form){ 23 | // Rename the save and continue button to Next 24 | var continueButton$ = $('input[name=_continue]'); 25 | continueButton$.val('Next Tab'); 26 | continueButton$.click((function(e) { 27 | e.preventDefault(); 28 | // Move to next tab unless we are on the last tab 29 | var selectedTab$ = $('.changeform-tabs li.selected'); 30 | 31 | if ($('.changeform-tabs li.selected:last-child').length === 0){ 32 | selectedTab$.removeClass('selected').next().addClass('selected'); 33 | var module = selectedTab$.index(); 34 | $('.module_'+module).removeClass('selected'); 35 | $('.module_'+(module+1)).addClass('selected'); 36 | set_button_navigation(); 37 | } 38 | })); 39 | $('.changeform-tabs li').click(function (e) { 40 | $('.changeform-tabs li').removeClass('selected'); 41 | $(e.currentTarget).addClass('selected'); 42 | 43 | set_button_navigation(); 44 | }); 45 | if($('.success').length){ 46 | var selectedTab$ = $('.changeform-tabs li.selected'); 47 | var module = selectedTab$.index(); 48 | selectedTab$.removeClass('selected'); 49 | $('.changeform-tabs li:first-child').addClass('selected'); 50 | $('.module_'+module).removeClass('selected'); 51 | $('.module_0').addClass('selected'); 52 | } 53 | set_button_navigation(); 54 | } 55 | }); 56 | }(jet.jQuery)); 57 | -------------------------------------------------------------------------------- /gmmp/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/base_site.html' %} 2 | {% load static i18n i18n_switcher jet_tags %} 3 | 4 | {% block extrastyle %} 5 | {{ block.super }} 6 | 7 | {% endblock %} 8 | 9 | {% block extrahead %} 10 | {{ block.super }} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% endblock %} 20 | 21 | {% block blockbots %} 22 | {{ block.super }} 23 | 24 | 26 | 27 | {% endblock %} 28 | 29 | {% block userlinks %} 30 | {{ block.super }} 31 | {% get_available_languages as LANGUAGES %} 32 | {% get_language_info_list for LANGUAGES as languages %} 33 | {% for language in languages %} 34 | 35 | {{ language.name_local }} 36 | 37 | {% endfor %} 38 | {% endblock %} 39 | 40 | {% block welcome-msg %} 41 | {% trans 'Welcome,' %} 42 | {{ user.monitor.country.alpha3 }} / {% firstof user.get_short_name user.get_username %} 43 | {% endblock %} 44 | 45 | {% block footer %} 46 |
47 |

Copyright © Global Media Monitoring Project 2020

48 |
49 | 67 | {% endblock %} 68 | -------------------------------------------------------------------------------- /reports/management/commands/import-historical.py: -------------------------------------------------------------------------------- 1 | from optparse import make_option 2 | 3 | from django.conf import settings 4 | from django.core.management.base import BaseCommand, CommandError 5 | 6 | from reports.historical import Historical 7 | from reports.report_details import REGION_COUNTRY_MAP, get_countries 8 | 9 | 10 | class Command(BaseCommand): 11 | help = "Import historical GMMP data from an XLSX file" 12 | 13 | def add_arguments(self, parser): 14 | parser.add_argument("filename", nargs="+", help="Excel file(s) to import") 15 | parser.add_argument("--global", action="store_true", help="Coverage is global") 16 | parser.add_argument( 17 | "--region", 18 | action="store", 19 | dest="region", 20 | help="Import historical data for a region. One of: {}".format( 21 | ", ".join(sorted(REGION_COUNTRY_MAP.keys())) 22 | ), 23 | ) 24 | parser.add_argument( 25 | "--country", 26 | action="store", 27 | dest="country", 28 | help="Import historical data for a country. One of: {}".format( 29 | ", ".join(sorted(c[0] for c in get_countries())) 30 | ), 31 | ) 32 | parser.add_argument( 33 | "--year", 34 | action="store", 35 | dest="year", 36 | help="Historical GMMP year the Excel file(s) belongs to. One of: 2010, 2015", 37 | ) 38 | 39 | def handle(self, *args, **options): 40 | historical = Historical() 41 | coverage = None 42 | region = None 43 | country = None 44 | year = options["year"] or settings.REPORTS_HISTORICAL_YEAR 45 | filenames = options["filename"] 46 | 47 | if year not in ["2010", "2015"]: 48 | raise ValueError("Invalid historical GMMP year: {}".format(year)) 49 | 50 | if options["global"]: 51 | coverage = "global" 52 | elif options["region"]: 53 | coverage = "region" 54 | region = options["region"] 55 | if region not in REGION_COUNTRY_MAP: 56 | raise ValueError("Unknown region: %s" % region) 57 | elif options["country"]: 58 | coverage = "country" 59 | country = options["country"] 60 | countries = {c: n for c, n in get_countries()} 61 | if not country.upper() in countries: 62 | for code, name in countries.items(): 63 | if name.upper() == country: 64 | country = code 65 | break 66 | if not country in countries: 67 | raise ValueError("Unknown country: %s" % country) 68 | else: 69 | raise CommandError("Must specify --global or --region") 70 | 71 | for fname in filenames: 72 | historical.import_from_file( 73 | fname, coverage, region=region, country=country, year=year 74 | ) 75 | 76 | historical.save() 77 | -------------------------------------------------------------------------------- /forms/migrations/0049_auto_20200820_0739.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.15 on 2020-08-20 07:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('forms', '0048_auto_20200820_0646'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='internetnewssheet', 15 | name='covid19', 16 | field=models.PositiveIntegerField(choices=[(1, '(1) Yes'), (2, '(2) No')], default=2, help_text='Note: For the question below it is important NOT to code COVID19-related stories under topic 23 but to choose the most relevant secondary topic theme in order to ensure results that can be compared with those from previous GMMPs.', verbose_name='(z) Is this story related to coronavirus Covid-19?'), 17 | ), 18 | migrations.AlterField( 19 | model_name='newspapersheet', 20 | name='covid19', 21 | field=models.PositiveIntegerField(choices=[(1, '(1) Yes'), (2, '(2) No')], default=2, help_text='Note: For the question below it is important NOT to code COVID19-related stories under topic 23 but to choose the most relevant secondary topic theme in order to ensure results that can be compared with those from previous GMMPs.', verbose_name='(z) Is this story related to coronavirus Covid-19?'), 22 | ), 23 | migrations.AlterField( 24 | model_name='radiosheet', 25 | name='covid19', 26 | field=models.PositiveIntegerField(choices=[(1, '(1) Yes'), (2, '(2) No')], default=2, help_text='Note: For the question below it is important NOT to code COVID19-related stories under topic 23 but to choose the most relevant secondary topic theme in order to ensure results that can be compared with those from previous GMMPs.', verbose_name='(z) Is this story related to coronavirus Covid-19?'), 27 | ), 28 | migrations.AlterField( 29 | model_name='televisionsheet', 30 | name='covid19', 31 | field=models.PositiveIntegerField(choices=[(1, '(1) Yes'), (2, '(2) No')], default=2, help_text='Note: For the question below it is important NOT to code COVID19-related stories under topic 23 but to choose the most relevant secondary topic theme in order to ensure results that can be compared with those from previous GMMPs.', verbose_name='(z) Is this story related to coronavirus Covid-19?'), 32 | ), 33 | migrations.AlterField( 34 | model_name='twittersheet', 35 | name='covid19', 36 | field=models.PositiveIntegerField(choices=[(1, '(1) Yes'), (2, '(2) No')], default=2, help_text='Note: For the question below it is important NOT to code COVID19-related stories under topic 23 but to choose the most relevant secondary topic theme in order to ensure results that can be compared with those from previous GMMPs.', verbose_name='(z) Is this story related to coronavirus Covid-19?'), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /forms/migrations/0059_update_country_region.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | COUNTRY_REGION = [ 7 | ("Myanmar", "Asia"), 8 | ("Angola","Africa"), 9 | ("Cambodia","Asia"), 10 | ("Cayman Islands","Caribbean"), 11 | ("Dominica","Caribbean"), 12 | ("Greenland","Europe"), 13 | ("Honduras","Latin America"), 14 | ("Hong Kong","Asia"), 15 | ("Iraq","Middle East"), 16 | ("Jordan","Middle East"), 17 | ("Macao","Asia"), 18 | ("Papua New Guinea","Pacific"), 19 | ("Russia","Europe"), 20 | ("Rwanda","Africa"), 21 | ("Seychelles","Africa"), 22 | ("Timor-Leste","Asia"), 23 | ("Uzbekistan","Asia"), 24 | ] 25 | 26 | def get_region_map(CountryRegion): 27 | from django_countries import countries 28 | 29 | region_map = {} 30 | 31 | # Map new country code(s) to regions 32 | for country_region in COUNTRY_REGION: 33 | code = countries.by_name(country_region[0]) 34 | if code: 35 | if country_region[1] in region_map: 36 | region_map[country_region[1]].append(code) 37 | else: 38 | region_map[country_region[1]] = [code] 39 | 40 | return region_map 41 | 42 | def code(apps, schema_editor): 43 | CountryRegion = apps.get_model("forms", "CountryRegion") 44 | db_alias = schema_editor.connection.alias 45 | 46 | # Update countries regions 47 | CountryRegion.objects.using(db_alias).filter(country="CY").update(region="Europe") 48 | CountryRegion.objects.using(db_alias).filter(country="KZ").update(region="Asia") 49 | CountryRegion.objects.using(db_alias).filter(country="PR").update(region="Caribbean") 50 | CountryRegion.objects.using(db_alias).filter(country="VU").update(region="Pacific") 51 | 52 | # Create CountryRegion objects for supplied pairs 53 | region_map = get_region_map(CountryRegion) 54 | for region, country_code_list in region_map.items(): 55 | for country_code in country_code_list: 56 | CountryRegion.objects.using(db_alias).create( 57 | country=country_code, region=region 58 | ) 59 | 60 | def reverse_code(apps, schema_editor): 61 | CountryRegion = apps.get_model("forms", "CountryRegion") 62 | db_alias = schema_editor.connection.alias 63 | # Reverse Update regions 64 | CountryRegion.objects.using(db_alias).filter(country="CY").update(region="Middle East") 65 | CountryRegion.objects.using(db_alias).filter(country="KZ").update(region="Europe") 66 | 67 | # Delete CountryRegion objects for supplied pairs 68 | region_map = get_region_map(CountryRegion) 69 | for region, country_code_list in region_map.items(): 70 | for country_code in country_code_list: 71 | CountryRegion.objects.using(db_alias).filter( 72 | country=country_code, region=region 73 | ).delete() 74 | 75 | 76 | class Migration(migrations.Migration): 77 | 78 | dependencies = [ 79 | ("forms", "0058_add_deleted_attribute"), 80 | ] 81 | 82 | operations = [ 83 | migrations.RunPython(code, reverse_code), 84 | ] 85 | -------------------------------------------------------------------------------- /forms/migrations/0043_update_country_region.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | COUNTRY_REGION = [ 7 | ("North Ireland", "Europe"), 8 | ("International", "Global"), 9 | ] 10 | 11 | 12 | def get_region_map(CountryRegion): 13 | from django_countries import countries 14 | 15 | region_map = {} 16 | 17 | # Map new country code(s) to regions 18 | for country_region in COUNTRY_REGION: 19 | code = countries.by_name(country_region[0]) 20 | if code: 21 | if country_region[1] in region_map: 22 | region_map[country_region[1]].append(code) 23 | else: 24 | region_map[country_region[1]] = [code] 25 | 26 | return region_map 27 | 28 | 29 | def code(apps, schema_editor): 30 | CountryRegion = apps.get_model("forms", "CountryRegion") 31 | db_alias = schema_editor.connection.alias 32 | 33 | country_region_objs = CountryRegion.objects.using(db_alias).all() 34 | 35 | # Update old custom country codes to ISO use-assignable codes 36 | CountryRegion.objects.using(db_alias).filter(country="B1").update(country="QM") 37 | CountryRegion.objects.using(db_alias).filter(country="B2").update(country="QN") 38 | CountryRegion.objects.using(db_alias).filter(country="EN").update(country="QO") 39 | CountryRegion.objects.using(db_alias).filter(country="SQ").update(country="QQ") 40 | CountryRegion.objects.using(db_alias).filter(country="WL").update(country="QR") 41 | 42 | # Create CountryRegion objects for supplied pairs 43 | region_map = get_region_map(CountryRegion) 44 | for region, country_code_list in region_map.items(): 45 | for country_code in country_code_list: 46 | CountryRegion.objects.using(db_alias).create( 47 | country=country_code, region=region 48 | ) 49 | 50 | 51 | def reverse_code(apps, schema_editor): 52 | CountryRegion = apps.get_model("forms", "CountryRegion") 53 | db_alias = schema_editor.connection.alias 54 | 55 | # Revert ISO use-assignable codes to old custom country codes 56 | CountryRegion.objects.using(db_alias).filter(country="QM").update(country="B1") 57 | CountryRegion.objects.using(db_alias).filter(country="QN").update(country="B2") 58 | CountryRegion.objects.using(db_alias).filter(country="QO").update(country="EN") 59 | CountryRegion.objects.using(db_alias).filter(country="QQ").update(country="SQ") 60 | CountryRegion.objects.using(db_alias).filter(country="QR").update(country="WL") 61 | 62 | # Delete CountryRegion objects for supplied pairs 63 | region_map = get_region_map(CountryRegion) 64 | for region, country_code_list in region_map.items(): 65 | for country_code in country_code_list: 66 | CountryRegion.objects.using(db_alias).filter( 67 | country=country_code, region=region 68 | ).delete() 69 | 70 | 71 | class Migration(migrations.Migration): 72 | 73 | dependencies = [ 74 | ("forms", "0042_auto_20200220_1247"), 75 | ] 76 | 77 | operations = [ 78 | migrations.RunPython(code, reverse_code), 79 | ] 80 | -------------------------------------------------------------------------------- /reports/views.py: -------------------------------------------------------------------------------- 1 | # Python 2 | from datetime import date 3 | 4 | # Django 5 | from django.views import View 6 | from django.shortcuts import render 7 | from django.http import HttpResponse 8 | from django.contrib.auth.decorators import login_required, user_passes_test 9 | from django.utils.decorators import method_decorator 10 | 11 | # Project 12 | from reports.report_builder import XLSXReportBuilder 13 | 14 | from reports.export_builder import XLSXDataExportBuilder 15 | from reports.forms import GlobalForm, CountryForm, RegionForm 16 | 17 | 18 | class ReportView(View): 19 | template_name = 'report_filter.html' 20 | 21 | @method_decorator(login_required) 22 | def dispatch(self, *args, **kwargs): 23 | return super(ReportView, self).dispatch(*args, **kwargs) 24 | 25 | def get(self, request, *args, **kwargs): 26 | country_form = CountryForm() 27 | region_form = RegionForm() 28 | context = { 29 | 'country_form': country_form, 30 | 'region_form': region_form} 31 | return render( 32 | request, 33 | self.template_name, 34 | context) 35 | 36 | def post(self, request, *args, **kwargs): 37 | if 'country_form' in request.POST: 38 | form = CountryForm(request.POST) 39 | choice = [country for code, country in form.get_form_countries() if code == request.POST['country']][0] 40 | elif 'region_form' in request.POST: 41 | form = RegionForm(request.POST) 42 | choice = [region for id, region in form.get_form_regions() if id == int(request.POST['region'])][0] 43 | else: 44 | form = GlobalForm(request.POST) 45 | choice = 'Global' 46 | 47 | if form.is_valid(): 48 | xlsx = XLSXReportBuilder(form).build() 49 | filename = 'GMMP Report: %s - %s' % (choice, date.today()) 50 | 51 | response = HttpResponse(xlsx, content_type='application/vnd.ms-excel') 52 | response['Content-Disposition'] = f'attachment; filename="{filename}.xlsx"' 53 | return response 54 | # context = {'form' : filter_form} 55 | # return render( 56 | # request, 57 | # self.template_name, 58 | # context) 59 | 60 | report_filter = CountryForm() 61 | context = {'form' : form} 62 | return render( 63 | request, 64 | self.template_name, 65 | context) 66 | 67 | 68 | @user_passes_test(lambda u: u.is_superuser) 69 | def data_export(request): 70 | if request.method == 'POST': 71 | xlsx = XLSXDataExportBuilder(request).build() 72 | filename = 'GMMP Data export' 73 | 74 | response = HttpResponse(xlsx, content_type='application/vnd.ms-excel') 75 | response['Content-Disposition'] = 'attachment; filename=%s.xlsx' % filename 76 | 77 | return response 78 | 79 | context = {} 80 | return render( 81 | request, 82 | 'data_export.html', 83 | context) 84 | 85 | 86 | class WazimapView(View): 87 | template_name = 'index.html' 88 | 89 | @method_decorator(login_required) 90 | def get(self, request, *args, **kwargs): 91 | return render( 92 | request, 93 | self.template_name, {}) 94 | -------------------------------------------------------------------------------- /gmmp/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Global Media Monitoring Project 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 67 | 68 | 69 |
70 |
71 | {% block page_content %} 72 | {% endblock page_content %} 73 | 74 |
75 |
76 |
77 |

Copyright © Global Media Monitoring Project 2020

78 |
79 |
80 | 81 |
82 | 83 |
84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /reports/historical/historical.py: -------------------------------------------------------------------------------- 1 | import openpyxl 2 | import json 3 | import os 4 | import logging 5 | import re 6 | 7 | from reports.report_details import WS_INFO 8 | 9 | from .canon import canon 10 | from ._gmmp_2010_importer import GMMP2010ReportImporter 11 | from ._gmmp_2015_importer import GMMP2015ReportImporter 12 | 13 | 14 | class Historical(object): 15 | log = logging.getLogger(__name__) 16 | 17 | def __init__(self, historical_file="historical.json"): 18 | self.fname = historical_file 19 | self.all_data = {} 20 | 21 | def load(self): 22 | if os.path.exists(self.fname): 23 | with open(self.fname, 'r') as f: 24 | self.all_data = json.load(f) 25 | 26 | def save(self): 27 | with open(self.fname, "w") as f: 28 | json.dump(self.all_data, f, indent=2, sort_keys=True) 29 | 30 | def get(self, new_ws, coverage, year, region=None, country=None): 31 | if coverage == "region": 32 | key = canon(region) 33 | elif coverage == "country": 34 | key = canon(country) 35 | elif coverage == "global": 36 | key = "global" 37 | else: 38 | raise ValueError("Unknown coverage %s" % coverage) 39 | 40 | sheet = WS_INFO["ws_" + new_ws][year] 41 | 42 | if "historical" not in sheet: 43 | raise KeyError( 44 | "New worksheet %s is not linked to an historical worksheet" % new_ws 45 | ) 46 | old_ws = sheet["historical"] 47 | 48 | if old_ws not in self.all_data[key]: 49 | raise KeyError( 50 | "Old worksheet %s does not have any historical data" % old_ws 51 | ) 52 | 53 | return self.all_data[key][sheet["historical"]] 54 | 55 | def import_from_file(self, fname, coverage, region=None, country=None, year=None): 56 | wb = openpyxl.load_workbook(fname, data_only=True) 57 | 58 | key = coverage 59 | if region: 60 | self.log.info("Importing for region %s", region) 61 | key = canon(region) 62 | elif country: 63 | self.log.info("Importing for country %s", country) 64 | key = canon(country) 65 | report_importer_by_year = { 66 | "2010": GMMP2010ReportImporter, 67 | "2015": GMMP2015ReportImporter, 68 | } 69 | report_importer = report_importer_by_year[year]() 70 | for sheet in self.historical_sheets(coverage, year): 71 | # find matching sheet name 72 | ws = report_importer.get_work_sheet(wb, sheet) 73 | if not ws: 74 | self.log.warn( 75 | "Couldn't find historical sheet %s; only have these sheets available: %s", 76 | sheet.get("historical"), 77 | ", ".join(sorted(wb.sheetnames)), 78 | ) 79 | continue 80 | 81 | self.log.info( 82 | "Importing sheet %s of the %s report", sheet.get("historical"), year 83 | ) 84 | data = report_importer.import_sheet(sheet) 85 | self.all_data.setdefault(key, {})[sheet.get("historical")] = data 86 | self.log.info( 87 | "Imported sheet %s of the %s report", sheet.get("historical"), year 88 | ) 89 | 90 | def historical_sheets(self, coverage, year): 91 | sheets = [] 92 | for sheets_by_year in WS_INFO.values(): 93 | sheet = sheets_by_year.get(year) 94 | if sheet and ("historical" in sheet and coverage in sheet["reports"]): 95 | sheets.append(sheet) 96 | return sheets 97 | -------------------------------------------------------------------------------- /forms/migrations/0058_add_deleted_attribute.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2020-10-01 06:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('forms', '0057_add_monitor_code'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='internetnewsjournalist', 15 | name='deleted', 16 | field=models.BooleanField(default=False, help_text='Mark this Journalist as deleted.'), 17 | ), 18 | migrations.AddField( 19 | model_name='internetnewsperson', 20 | name='deleted', 21 | field=models.BooleanField(default=False, help_text='Mark this person as deleted.'), 22 | ), 23 | migrations.AddField( 24 | model_name='internetnewssheet', 25 | name='deleted', 26 | field=models.BooleanField(default=False, help_text='Mark this sheet as deleted.'), 27 | ), 28 | migrations.AddField( 29 | model_name='newspaperjournalist', 30 | name='deleted', 31 | field=models.BooleanField(default=False, help_text='Mark this Journalist as deleted.'), 32 | ), 33 | migrations.AddField( 34 | model_name='newspaperperson', 35 | name='deleted', 36 | field=models.BooleanField(default=False, help_text='Mark this person as deleted.'), 37 | ), 38 | migrations.AddField( 39 | model_name='newspapersheet', 40 | name='deleted', 41 | field=models.BooleanField(default=False, help_text='Mark this sheet as deleted.'), 42 | ), 43 | migrations.AddField( 44 | model_name='radiojournalist', 45 | name='deleted', 46 | field=models.BooleanField(default=False, help_text='Mark this Journalist as deleted.'), 47 | ), 48 | migrations.AddField( 49 | model_name='radioperson', 50 | name='deleted', 51 | field=models.BooleanField(default=False, help_text='Mark this person as deleted.'), 52 | ), 53 | migrations.AddField( 54 | model_name='radiosheet', 55 | name='deleted', 56 | field=models.BooleanField(default=False, help_text='Mark this sheet as deleted.'), 57 | ), 58 | migrations.AddField( 59 | model_name='televisionjournalist', 60 | name='deleted', 61 | field=models.BooleanField(default=False, help_text='Mark this Journalist as deleted.'), 62 | ), 63 | migrations.AddField( 64 | model_name='televisionperson', 65 | name='deleted', 66 | field=models.BooleanField(default=False, help_text='Mark this person as deleted.'), 67 | ), 68 | migrations.AddField( 69 | model_name='televisionsheet', 70 | name='deleted', 71 | field=models.BooleanField(default=False, help_text='Mark this sheet as deleted.'), 72 | ), 73 | migrations.AddField( 74 | model_name='twitterjournalist', 75 | name='deleted', 76 | field=models.BooleanField(default=False, help_text='Mark this Journalist as deleted.'), 77 | ), 78 | migrations.AddField( 79 | model_name='twitterperson', 80 | name='deleted', 81 | field=models.BooleanField(default=False, help_text='Mark this person as deleted.'), 82 | ), 83 | migrations.AddField( 84 | model_name='twittersheet', 85 | name='deleted', 86 | field=models.BooleanField(default=False, help_text='Mark this sheet as deleted.'), 87 | ), 88 | ] 89 | -------------------------------------------------------------------------------- /forms/migrations/0003_auto_20150309_1340.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0002_auto_20150309_1227'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='televisionsheet', 16 | name='person_secondary', 17 | field=models.PositiveIntegerField(default=None, help_text="

\n Select ''Secondary Source'' only if the story is based solely on information from a report, article, or other piece of written information.

\nCode information for:
\n - Any person whom the story is about even if they are not interviewed or quoted
\n - Each person who is interviewed
\n - Each person in the story who is quoted, either directly or indirectly. Code only individual people.
\n
\nDo not code:\n - Groups (e.g. a group of nurses, a group of soldiers);
\n - Organisations, companies, collectivities (e.g. political parties);
\n - Characters in novels or movies (unless the story is about them);
\n - Deceased historical figures (unless the story is about them);
\n - Interpreters (Code the person being interviewed as if they spoke without an interpreter).
\n", verbose_name='Source', choices=[(1, 'Person'), (2, 'Secondary Source')]), 18 | preserve_default=False, 19 | ), 20 | migrations.AlterField( 21 | model_name='radiosheet', 22 | name='num_female_anchors', 23 | field=models.PositiveIntegerField(help_text='The anchor (or announcer, or presenter) is the person who introduces the newscast and the individual items within it. Note: You should only include the anchors/announcers. Do not include reporters or other', verbose_name='Number of female anchors', choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9)]), 24 | preserve_default=True, 25 | ), 26 | migrations.AlterField( 27 | model_name='radiosheet', 28 | name='num_male_anchors', 29 | field=models.PositiveIntegerField(help_text='The anchor (or announcer, or presenter) is the person who introduces the newscast and the individual items within it. Note: You should only include the anchors/announcers. Do not include reporters or other journalists', verbose_name='Number of male anchors', choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9)]), 30 | preserve_default=True, 31 | ), 32 | migrations.AlterField( 33 | model_name='televisionsheet', 34 | name='num_female_anchors', 35 | field=models.PositiveIntegerField(help_text='The anchor (or announcer, or presenter) is the person who introduces the newscast and the individual items within it. Note: You should only include the anchors/announcers. Do not include reporters or other', verbose_name='Number of female anchors', choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9)]), 36 | preserve_default=True, 37 | ), 38 | migrations.AlterField( 39 | model_name='televisionsheet', 40 | name='num_male_anchors', 41 | field=models.PositiveIntegerField(help_text='The anchor (or announcer, or presenter) is the person who introduces the newscast and the individual items within it. Note: You should only include the anchors/announcers. Do not include reporters or other journalists', verbose_name='Number of male anchors', choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9)]), 42 | preserve_default=True, 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /forms/migrations/0018_auto_20150416_0855.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0017_auto_20150331_1815'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='internetnewssheet', 16 | name='webpage_layer_no', 17 | field=models.PositiveIntegerField(help_text='Webpage Layer Number. Homepage=1, One click away=2, Five clicks away= 5, etc. Note that if a story appears on the front page, code with 1', verbose_name='(1) Webpage Layer Number'), 18 | preserve_default=True, 19 | ), 20 | migrations.AlterField( 21 | model_name='newspapersheet', 22 | name='page_number', 23 | field=models.PositiveIntegerField(help_text='Write in the number of the page on which the story begins. Story appears on first page = 1, Seventh page = 7, etc.', verbose_name='(1) Page Number'), 24 | preserve_default=True, 25 | ), 26 | migrations.AlterField( 27 | model_name='radiosheet', 28 | name='item_number', 29 | field=models.PositiveIntegerField(help_text='Write in the number that describes the position of the story within the newscast. E.g. the first story in the newscast is item 1; the seventh story is item 7.', verbose_name='(1) Item Number'), 30 | preserve_default=True, 31 | ), 32 | migrations.AlterField( 33 | model_name='radiosheet', 34 | name='num_female_anchors', 35 | field=models.PositiveIntegerField(help_text='The anchor (or announcer, or presenter) is the person who introduces the newscast and the individual items within it. Note: You should only include the anchors/announcers. Do not include reporters or other', verbose_name='Number of female anchors'), 36 | preserve_default=True, 37 | ), 38 | migrations.AlterField( 39 | model_name='radiosheet', 40 | name='num_male_anchors', 41 | field=models.PositiveIntegerField(help_text='The anchor (or announcer, or presenter) is the person who introduces the newscast and the individual items within it. Note: You should only include the anchors/announcers. Do not include reporters or other journalists', verbose_name='Number of male anchors'), 42 | preserve_default=True, 43 | ), 44 | migrations.AlterField( 45 | model_name='televisionsheet', 46 | name='item_number', 47 | field=models.PositiveIntegerField(help_text='Write in the number that describes the position of the story within the newscast. E.g. the first story in the newscast is item 1; the seventh story is item 7.', verbose_name='(1) Item Number'), 48 | preserve_default=True, 49 | ), 50 | migrations.AlterField( 51 | model_name='televisionsheet', 52 | name='num_female_anchors', 53 | field=models.PositiveIntegerField(help_text='The anchor (or announcer, or presenter) is the person who introduces the newscast and the individual items within it. Note: You should only include the anchors/announcers. Do not include reporters or other', verbose_name='Number of female anchors'), 54 | preserve_default=True, 55 | ), 56 | migrations.AlterField( 57 | model_name='televisionsheet', 58 | name='num_male_anchors', 59 | field=models.PositiveIntegerField(help_text='The anchor (or announcer, or presenter) is the person who introduces the newscast and the individual items within it. Note: You should only include the anchors/announcers. Do not include reporters or other journalists', verbose_name='Number of male anchors'), 60 | preserve_default=True, 61 | ), 62 | ] 63 | -------------------------------------------------------------------------------- /forms/migrations/0019_auto_20150506_1052.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0018_auto_20150416_0855'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='internetnewssheet', 16 | name='stereotypes', 17 | field=models.PositiveIntegerField(help_text='This story clearly challenges gender stereotypes', verbose_name='(23) Challenges Stereotypes', choices=[(1, '(1) Agree'), (2, '(2) Disagree'), (3, '(3) Neither agree nor disagree'), (4, '(4) Do not know')]), 18 | preserve_default=True, 19 | ), 20 | migrations.AlterField( 21 | model_name='newspapersheet', 22 | name='space', 23 | field=models.PositiveIntegerField(verbose_name='(4) Space', choices=[(1, '(1) Full page'), (2, '(2) Half page'), (3, '(3) One third page'), (4, '(4) Quarter page'), (5, '(5) Less than quarter page')]), 24 | preserve_default=True, 25 | ), 26 | migrations.AlterField( 27 | model_name='newspapersheet', 28 | name='stereotypes', 29 | field=models.PositiveIntegerField(help_text='This story clearly challenges gender stereotypes', verbose_name='(21) Challenges Stereotypes', choices=[(1, '(1) Agree'), (2, '(2) Disagree'), (3, '(3) Neither agree nor disagree'), (4, '(4) Do not know')]), 30 | preserve_default=True, 31 | ), 32 | migrations.AlterField( 33 | model_name='radiojournalist', 34 | name='role', 35 | field=models.PositiveIntegerField(verbose_name='Role', choices=[(1, '(1) Anchor, announcer or presenter: Usually in the television studio'), (2, '(2) Reporter: Usually outside the studio. Include reporters who do not appear on screen, but whose voice is heard (e.g. as voice-over).'), (3, '(3) Other journalist: Sportscaster, weather forecaster, commentator/analyst etc.')]), 36 | preserve_default=True, 37 | ), 38 | migrations.AlterField( 39 | model_name='radiosheet', 40 | name='stereotypes', 41 | field=models.PositiveIntegerField(help_text='This story clearly challenges gender stereotypes', verbose_name='(16) Challenges Stereotypes', choices=[(1, '(1) Agree'), (2, '(2) Disagree'), (3, '(3) Neither agree nor disagree'), (4, '(4) Do not know')]), 42 | preserve_default=True, 43 | ), 44 | migrations.AlterField( 45 | model_name='televisionjournalist', 46 | name='role', 47 | field=models.PositiveIntegerField(verbose_name='Role', choices=[(1, '(1) Anchor, announcer or presenter: Usually in the television studio'), (2, '(2) Reporter: Usually outside the studio. Include reporters who do not appear on screen, but whose voice is heard (e.g. as voice-over).'), (3, '(3) Other journalist: Sportscaster, weather forecaster, commentator/analyst etc.')]), 48 | preserve_default=True, 49 | ), 50 | migrations.AlterField( 51 | model_name='televisionsheet', 52 | name='stereotypes', 53 | field=models.PositiveIntegerField(help_text='This story clearly challenges gender stereotypes', verbose_name='(18) Challenges Stereotypes', choices=[(1, '(1) Agree'), (2, '(2) Disagree'), (3, '(3) Neither agree nor disagree'), (4, '(4) Do not know')]), 54 | preserve_default=True, 55 | ), 56 | migrations.AlterField( 57 | model_name='twittersheet', 58 | name='stereotypes', 59 | field=models.PositiveIntegerField(help_text='This tweet clearly challenges gender stereotypes', verbose_name='(8) Challenges Stereotypes', choices=[(1, '(1) Agree'), (2, '(2) Disagree'), (3, '(3) Neither agree nor disagree'), (4, '(4) Do not know')]), 60 | preserve_default=True, 61 | ), 62 | ] 63 | -------------------------------------------------------------------------------- /gmmp/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block page_content %} 5 | 6 |
7 |
8 |

9 |

Welcome to the 2020 GMMP Database.

10 |

Select one of the following forms to capture the relevant information.

11 |
12 |
13 | 14 | 15 | 16 |
17 |
18 | 24 |

25 | Internet News 26 |

27 |

Internet News: Capture here news that are online. Remember to only capture national (and if necessary, local) major websites.

28 |
29 |
30 | 36 |

37 | Newspapers 38 |

39 |

Newspapers: Newspaper news are news published on print. The number of newspapers you code will depend on the number of newspapers in your country.

40 |
41 |
42 | 48 |

49 | Twitter 50 |

51 |

Twitter: Only capture tweets that are specific posts/feeds by news media on twitter. Remember to capture only national (and if necessary, local) media house Twitter feeds.

52 |
53 |
54 | 55 | 56 | 57 |
58 |
59 | 65 |

66 | Radio News 67 |

68 |

Radio News: Capture here news that are broadcast on the radio. The number of newscasts you code will depend on the number of radio channels that broadcast news in your country.

69 |
70 |
71 | 77 |

78 | Television News 79 |

80 |

Televison News: Capture here news that are broadcast on television. The number of newscast you code will depend on the number of television channels that broadcast news in your country.

81 |
82 |
83 | 89 |
90 |
91 | {% endblock page_content %} 92 | -------------------------------------------------------------------------------- /forms/migrations/0004_auto_20150309_1358.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('forms', '0003_auto_20150309_1340'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='internetnewsperson', 16 | name='age', 17 | ), 18 | migrations.RemoveField( 19 | model_name='internetnewsperson', 20 | name='is_photograph', 21 | ), 22 | migrations.RemoveField( 23 | model_name='internetnewsperson', 24 | name='is_quoted', 25 | ), 26 | migrations.RemoveField( 27 | model_name='internetnewsperson', 28 | name='occupation_other', 29 | ), 30 | migrations.RemoveField( 31 | model_name='internetnewsperson', 32 | name='survivor_comments', 33 | ), 34 | migrations.RemoveField( 35 | model_name='internetnewsperson', 36 | name='victim_comments', 37 | ), 38 | migrations.RemoveField( 39 | model_name='newspaperperson', 40 | name='occupation_other', 41 | ), 42 | migrations.RemoveField( 43 | model_name='newspaperperson', 44 | name='survivor_comments', 45 | ), 46 | migrations.RemoveField( 47 | model_name='newspaperperson', 48 | name='victim_comments', 49 | ), 50 | migrations.RemoveField( 51 | model_name='radioperson', 52 | name='age', 53 | ), 54 | migrations.RemoveField( 55 | model_name='radioperson', 56 | name='is_photograph', 57 | ), 58 | migrations.RemoveField( 59 | model_name='radioperson', 60 | name='is_quoted', 61 | ), 62 | migrations.RemoveField( 63 | model_name='radioperson', 64 | name='occupation_other', 65 | ), 66 | migrations.RemoveField( 67 | model_name='radioperson', 68 | name='survivor_comments', 69 | ), 70 | migrations.RemoveField( 71 | model_name='radioperson', 72 | name='victim_comments', 73 | ), 74 | migrations.RemoveField( 75 | model_name='televisionperson', 76 | name='is_photograph', 77 | ), 78 | migrations.RemoveField( 79 | model_name='televisionperson', 80 | name='is_quoted', 81 | ), 82 | migrations.RemoveField( 83 | model_name='televisionperson', 84 | name='occupation_other', 85 | ), 86 | migrations.RemoveField( 87 | model_name='televisionperson', 88 | name='survivor_comments', 89 | ), 90 | migrations.RemoveField( 91 | model_name='televisionperson', 92 | name='victim_comments', 93 | ), 94 | migrations.RemoveField( 95 | model_name='twitterperson', 96 | name='age', 97 | ), 98 | migrations.RemoveField( 99 | model_name='twitterperson', 100 | name='family_role', 101 | ), 102 | migrations.RemoveField( 103 | model_name='twitterperson', 104 | name='function', 105 | ), 106 | migrations.RemoveField( 107 | model_name='twitterperson', 108 | name='is_quoted', 109 | ), 110 | migrations.RemoveField( 111 | model_name='twitterperson', 112 | name='occupation', 113 | ), 114 | migrations.RemoveField( 115 | model_name='twitterperson', 116 | name='occupation_other', 117 | ), 118 | migrations.RemoveField( 119 | model_name='twitterperson', 120 | name='survivor_comments', 121 | ), 122 | migrations.RemoveField( 123 | model_name='twitterperson', 124 | name='survivor_of', 125 | ), 126 | migrations.RemoveField( 127 | model_name='twitterperson', 128 | name='victim_comments', 129 | ), 130 | migrations.RemoveField( 131 | model_name='twitterperson', 132 | name='victim_of', 133 | ), 134 | migrations.RemoveField( 135 | model_name='twitterperson', 136 | name='victim_or_survivor', 137 | ), 138 | ] 139 | -------------------------------------------------------------------------------- /forms/migrations/0051_increate_people_in_the_news_age.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2020-09-07 06:08 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('forms', '0050_add_created_and_updated_at'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='internetnewsperson', 15 | name='age', 16 | field=models.PositiveIntegerField(choices=[(0, '(0) Do not know'), (1, '(1) 12 and under'), (2, '(2) 13-18'), (3, '(3) 19-34'), (4, '(4) 35-49'), (5, '(5) 50-64'), (6, '(6) 65-79'), (7, '(7) 80 years or more')], verbose_name='(13) Age (person appears)'), 17 | ), 18 | migrations.AlterField( 19 | model_name='internetnewssheet', 20 | name='covid19', 21 | field=models.PositiveIntegerField(choices=[(1, '(1) Yes'), (2, '(2) No')], help_text='Note: For the question below it is important NOT to code COVID19-related stories under topic 23 but to choose the most relevant secondary topic theme in order to ensure results that can be compared with those from previous GMMPs.', verbose_name='(z) Is this story related to coronavirus Covid-19?'), 22 | ), 23 | migrations.AlterField( 24 | model_name='newspaperperson', 25 | name='age', 26 | field=models.PositiveIntegerField(choices=[(0, '(0) Do not know'), (1, '(1) 12 and under'), (2, '(2) 13-18'), (3, '(3) 19-34'), (4, '(4) 35-49'), (5, '(5) 50-64'), (6, '(6) 65-79'), (7, '(7) 80 years or more')], verbose_name='(11) Age (person appears)'), 27 | ), 28 | migrations.AlterField( 29 | model_name='newspapersheet', 30 | name='covid19', 31 | field=models.PositiveIntegerField(choices=[(1, '(1) Yes'), (2, '(2) No')], help_text='Note: For the question below it is important NOT to code COVID19-related stories under topic 23 but to choose the most relevant secondary topic theme in order to ensure results that can be compared with those from previous GMMPs.', verbose_name='(z) Is this story related to coronavirus Covid-19?'), 32 | ), 33 | migrations.AlterField( 34 | model_name='radiosheet', 35 | name='covid19', 36 | field=models.PositiveIntegerField(choices=[(1, '(1) Yes'), (2, '(2) No')], help_text='Note: For the question below it is important NOT to code COVID19-related stories under topic 23 but to choose the most relevant secondary topic theme in order to ensure results that can be compared with those from previous GMMPs.', verbose_name='(z) Is this story related to coronavirus Covid-19?'), 37 | ), 38 | migrations.AlterField( 39 | model_name='televisionperson', 40 | name='age', 41 | field=models.PositiveIntegerField(choices=[(0, '(0) Do not know'), (1, '(1) 12 and under'), (2, '(2) 13-18'), (3, '(3) 19-34'), (4, '(4) 35-49'), (5, '(5) 50-64'), (6, '(6) 65-79'), (7, '(7) 80 years or more')], verbose_name='(12) Age (person appears)'), 42 | ), 43 | migrations.AlterField( 44 | model_name='televisionsheet', 45 | name='covid19', 46 | field=models.PositiveIntegerField(choices=[(1, '(1) Yes'), (2, '(2) No')], help_text='Note: For the question below it is important NOT to code COVID19-related stories under topic 23 but to choose the most relevant secondary topic theme in order to ensure results that can be compared with those from previous GMMPs.', verbose_name='(z) Is this story related to coronavirus Covid-19?'), 47 | ), 48 | migrations.AlterField( 49 | model_name='twitterperson', 50 | name='age', 51 | field=models.PositiveIntegerField(choices=[(0, '(0) Do not know'), (1, '(1) 12 and under'), (2, '(2) 13-18'), (3, '(3) 19-34'), (4, '(4) 35-49'), (5, '(5) 50-64'), (6, '(6) 65-79'), (7, '(7) 80 years or more')], verbose_name='(10) Age (person appears)'), 52 | ), 53 | migrations.AlterField( 54 | model_name='twittersheet', 55 | name='covid19', 56 | field=models.PositiveIntegerField(choices=[(1, '(1) Yes'), (2, '(2) No')], help_text='Note: For the question below it is important NOT to code COVID19-related stories under topic 23 but to choose the most relevant secondary topic theme in order to ensure results that can be compared with those from previous GMMPs.', verbose_name='(z) Is this story related to coronavirus Covid-19?'), 57 | ), 58 | ] 59 | -------------------------------------------------------------------------------- /forms/migrations/0046_auto_20200312_0949.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-03-12 09:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('forms', '0045_auto_20200306_1510'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='internetnewsperson', 15 | name='is_quoted', 16 | field=models.CharField(choices=[('Y', '(1) Yes'), ('N', '(2) No')], help_text='A person is directly quoted if their own words are printed, e.g. "The war against terror is our first priority" said President Bush.
If the story paraphrases what the person said, that is not a direct quote, e.g. President Bush said that top priority would be given to fighting the war against terror.', max_length=1, verbose_name='(20) Is the person directly quoted'), 17 | ), 18 | migrations.AlterField( 19 | model_name='internetnewsperson', 20 | name='victim_or_survivor', 21 | field=models.CharField(choices=[('Y', '(1) Yes'), ('N', '(2) No')], help_text="You should code a person as a victim either if the word 'victim' is used to describe her/him, or if the story Implies that the person is a victim - e.g. by using language or images that evoke particular emotions such as shock, horror, pity for the person.
You should code a person as a survivor either if the word 'survivor' is used to describe her/him, or if the story implies that the person is a survivor - e.g. by using language or images that evoke particular emotions such as admiration or respect for the person.", max_length=1, verbose_name='(17) Does the story identify the person as either a victim or survivor?'), 22 | ), 23 | migrations.AlterField( 24 | model_name='newspaperperson', 25 | name='is_quoted', 26 | field=models.CharField(choices=[('Y', '(1) Yes'), ('N', '(2) No')], help_text='A person is directly quoted if their own words are printed, e.g. "The war against terror is our first priority" said President Bush.
If the story paraphrases what the person said, that is not a direct quote, e.g. President Bush said that top priority would be given to fighting the war against terror.', max_length=1, verbose_name='(18) Is the person directly quoted'), 27 | ), 28 | migrations.AlterField( 29 | model_name='newspaperperson', 30 | name='victim_or_survivor', 31 | field=models.CharField(choices=[('Y', '(1) Yes'), ('N', '(2) No')], help_text="You should code a person as a victim either if the word 'victim' is used to describe her/him, or if the story Implies that the person is a victim - e.g. by using language or images that evoke particular emotions such as shock, horror, pity for the person.
You should code a person as a survivor either if the word 'survivor' is used to describe her/him, or if the story implies that the person is a survivor - e.g. by using language or images that evoke particular emotions such as admiration or respect for the person.", max_length=1, verbose_name='(15) Does the story identify the person as either a victim or survivor?'), 32 | ), 33 | migrations.AlterField( 34 | model_name='radioperson', 35 | name='victim_or_survivor', 36 | field=models.CharField(choices=[('Y', '(1) Yes'), ('N', '(2) No')], help_text="You should code a person as a victim either if the word 'victim' is used to describe her/him, or if the story Implies that the person is a victim - e.g. by using language or images that evoke particular emotions such as shock, horror, pity for the person.
You should code a person as a survivor either if the word 'survivor' is used to describe her/him, or if the story implies that the person is a survivor - e.g. by using language or images that evoke particular emotions such as admiration or respect for the person.", max_length=1, verbose_name='(14) Does the story identify the person as either a victim or survivor?'), 37 | ), 38 | migrations.AlterField( 39 | model_name='televisionperson', 40 | name='victim_or_survivor', 41 | field=models.CharField(choices=[('Y', '(1) Yes'), ('N', '(2) No')], help_text="You should code a person as a victim either if the word 'victim' is used to describe her/him, or if the story Implies that the person is a victim - e.g. by using language or images that evoke particular emotions such as shock, horror, pity for the person.
You should code a person as a survivor either if the word 'survivor' is used to describe her/him, or if the story implies that the person is a survivor - e.g. by using language or images that evoke particular emotions such as admiration or respect for the person.", max_length=1, verbose_name='(16) Does the story identify the person as either a victim or survivor?'), 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /forms/migrations/0042_auto_20200220_1247.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.9 on 2020-02-20 12:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('forms', '0041_auto_20200123_1224'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='internetnewssheet', 15 | options={'verbose_name': 'Internet'}, 16 | ), 17 | migrations.AlterModelOptions( 18 | name='newspapersheet', 19 | options={'verbose_name': 'Newspaper'}, 20 | ), 21 | migrations.AlterModelOptions( 22 | name='radiosheet', 23 | options={'verbose_name': 'Radio'}, 24 | ), 25 | migrations.AlterModelOptions( 26 | name='televisionsheet', 27 | options={'verbose_name': 'Television'}, 28 | ), 29 | migrations.AlterModelOptions( 30 | name='twittersheet', 31 | options={'verbose_name': 'Twitter'}, 32 | ), 33 | migrations.AlterField( 34 | model_name='internetnewsperson', 35 | name='special_qn_1', 36 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(22) Special question (1)'), 37 | ), 38 | migrations.AlterField( 39 | model_name='internetnewsperson', 40 | name='special_qn_2', 41 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(23) Special question (2)'), 42 | ), 43 | migrations.AlterField( 44 | model_name='internetnewsperson', 45 | name='special_qn_3', 46 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(24) Special question (3)'), 47 | ), 48 | migrations.AlterField( 49 | model_name='newspaperperson', 50 | name='special_qn_1', 51 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(20) Special question (1)'), 52 | ), 53 | migrations.AlterField( 54 | model_name='newspaperperson', 55 | name='special_qn_2', 56 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(21) Special question (2)'), 57 | ), 58 | migrations.AlterField( 59 | model_name='newspaperperson', 60 | name='special_qn_3', 61 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(22) Special question (3)'), 62 | ), 63 | migrations.AlterField( 64 | model_name='radioperson', 65 | name='special_qn_1', 66 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(17) Special question (1)'), 67 | ), 68 | migrations.AlterField( 69 | model_name='radioperson', 70 | name='special_qn_2', 71 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(18) Special question (2)'), 72 | ), 73 | migrations.AlterField( 74 | model_name='radioperson', 75 | name='special_qn_3', 76 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(19) Special question (3)'), 77 | ), 78 | migrations.AlterField( 79 | model_name='televisionperson', 80 | name='special_qn_1', 81 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(19) Special question (1)'), 82 | ), 83 | migrations.AlterField( 84 | model_name='televisionperson', 85 | name='special_qn_2', 86 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(20) Special question (2)'), 87 | ), 88 | migrations.AlterField( 89 | model_name='televisionperson', 90 | name='special_qn_3', 91 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(21) Special question (3)'), 92 | ), 93 | migrations.AlterField( 94 | model_name='twitterperson', 95 | name='special_qn_1', 96 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(14) Special question (1)'), 97 | ), 98 | migrations.AlterField( 99 | model_name='twitterperson', 100 | name='special_qn_2', 101 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(15) Special question (2)'), 102 | ), 103 | migrations.AlterField( 104 | model_name='twitterperson', 105 | name='special_qn_3', 106 | field=models.CharField(blank=True, choices=[('Y', '(1) Yes'), ('N', '(2) No')], max_length=1, verbose_name='(16) Special question (3)'), 107 | ), 108 | ] 109 | -------------------------------------------------------------------------------- /reports/historical/_base_importer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Holds the base import class which holds methods which are 3 | common to all year import classes. It also holds utility functions 4 | """ 5 | import re 6 | 7 | from .canon import canon 8 | 9 | 10 | def v(v): 11 | """ 12 | Try to interpret a value as an percentage 13 | """ 14 | try: 15 | basestring 16 | except NameError: 17 | basestring = str 18 | if not isinstance(v, basestring): 19 | return v 20 | 21 | m = re.match(r"^(\d+(\.\d+)?)(%)?$", v) 22 | if m: 23 | if not m.group(2): 24 | # int 25 | v = int(m.group(1)) 26 | else: 27 | # float 28 | v = float(m.group(1)) 29 | 30 | if m.group(3): 31 | v = v / 100.0 32 | return v 33 | 34 | 35 | class BaseReportImporter(object): 36 | def __init__(self): 37 | self.ws = None 38 | 39 | def get_work_sheet(self, wb, sheet): 40 | for name in wb.sheetnames: 41 | if name == sheet.get("historical"): 42 | self.ws = wb[name] 43 | return self.ws 44 | 45 | def import_sheet(self, sheet): 46 | return getattr(self, "import_%s" % sheet.get("historical"))(sheet) 47 | 48 | def slurp_secondary_col_table( 49 | self, 50 | ws, 51 | data, 52 | col_start, 53 | cols_per_group, 54 | cols, 55 | row_end, 56 | row_start=5, 57 | major_col_heading_row=4, 58 | row_heading_col=5, 59 | ): 60 | """ 61 | Get values from a table with two levels of column headings. 62 | 63 | eg. 64 | Major 1 | Major 2 65 | Col 1 | Col 2 | Col 1 | Col 2 66 | row1 67 | row2 68 | row3 69 | """ 70 | for icol in range(col_start, col_start + cols * cols_per_group, cols_per_group): 71 | major_col_heading = canon( 72 | ws.cell(column=icol, row=major_col_heading_row).value 73 | ) 74 | if not major_col_heading: 75 | continue 76 | major_col_data = {} 77 | data[major_col_heading] = major_col_data 78 | 79 | self.slurp_table( 80 | ws, 81 | major_col_data, 82 | icol, 83 | icol + cols_per_group - 1, 84 | row_end, 85 | row_start=row_start, 86 | col_heading_row=major_col_heading_row + 1, 87 | row_heading_col=row_heading_col, 88 | ) 89 | 90 | def slurp_table( 91 | self, 92 | ws, 93 | data, 94 | col_start, 95 | col_end, 96 | row_end, 97 | row_start, 98 | col_heading_row, 99 | row_heading_col, 100 | ): 101 | """ 102 | Grab values from a simple table with column and row titles. 103 | 104 | eg. 105 | Cat 1 | Cat 2 | N 106 | row1 107 | row2 108 | row3 109 | """ 110 | for icol in range(col_start, col_end + 1): 111 | col_heading = canon(ws.cell(column=icol, row=col_heading_row).value) 112 | if not col_heading: 113 | continue 114 | col_data = {} 115 | data[col_heading] = col_data 116 | 117 | for irow in range(row_start, row_end + 1): 118 | row_heading = canon(ws.cell(column=row_heading_col, row=irow).value) 119 | col_data[row_heading] = v(ws.cell(column=icol, row=irow).value) 120 | 121 | def slurp_year_grouped_table( 122 | self, 123 | ws, 124 | all_data, 125 | col_start, 126 | cols_per_group, 127 | cols, 128 | row_end, 129 | row_start=5, 130 | year_heading_row=4, 131 | col_heading_row=3, 132 | row_heading_col=5, 133 | skip_years=[], 134 | ): 135 | """ 136 | Slurp a table where each category contains a range of years. 137 | 138 | eg. 139 | Category 1 | Category 2 | 140 | 2005 | 2010 | N | 2005 | 2010 | N | 141 | row1 142 | row2 143 | row3 144 | """ 145 | for icol in range(col_start, col_start + cols * cols_per_group, cols_per_group): 146 | col_heading = canon(ws.cell(column=icol, row=col_heading_row).value) 147 | 148 | for iyear in range(icol, icol + cols_per_group): 149 | year = ws.cell(column=iyear, row=year_heading_row).value 150 | if year in ["N", "N-F"]: 151 | year = 2010 152 | effective_col_heading = canon("N") 153 | else: 154 | year = int(year) 155 | effective_col_heading = col_heading 156 | 157 | if year not in skip_years: 158 | data = all_data.setdefault(year, {}) 159 | col_data = data.setdefault(effective_col_heading, {}) 160 | 161 | for irow in range(row_start, row_end + 1): 162 | row_heading = canon( 163 | ws.cell(column=row_heading_col, row=irow).value 164 | ) 165 | col_data[row_heading] = v(ws.cell(column=iyear, row=irow).value) 166 | -------------------------------------------------------------------------------- /gmmp/dashboard_modules.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.utils.translation import gettext_lazy as _ 3 | from jet.dashboard.modules import DashboardModule 4 | 5 | from forms.models import ( 6 | InternetNewsSheet, 7 | NewspaperSheet, 8 | RadioSheet, 9 | TelevisionSheet, 10 | TwitterSheet, 11 | ) 12 | 13 | 14 | class AddInternetNewsSubmission(DashboardModule): 15 | title = _("Internet") 16 | url = reverse("admin:forms_internetnewssheet_add") 17 | template = "gmmp/dashboard_modules/add_submission.html" 18 | 19 | def init_with_context(self, context): 20 | self.children = [ 21 | { 22 | "title": _("Code Story"), 23 | "url": reverse("admin:forms_internetnewssheet_add"), 24 | "description": _('''Code all online news content from the home page or 'first layer' of the site that are not designated as health/sports/entertainment/business news unless it is apparent that they are 25 | uncharacteristically important stories that day (i.e. would appear in the front page section of a newspaper instead of the appropriate sub-section).'''), 26 | }, 27 | ] 28 | 29 | 30 | class AddNewspaperSubmission(DashboardModule): 31 | title = _("Newspapers") 32 | url = reverse("admin:forms_newspapersheet_add") 33 | template = "gmmp/dashboard_modules/add_submission.html" 34 | 35 | def init_with_context(self, context): 36 | self.children = [ 37 | { 38 | "title": _("Code Story"), 39 | "url": reverse("admin:forms_newspapersheet_add"), 40 | "description": _('''Begin with the main news page (usually Page 1). Code all the news stories on this page. Then go to the next major news page. 41 | Code regular news stories only - not editorials, commentaries, letters to the editor.'''), 42 | }, 43 | ] 44 | 45 | 46 | class AddRadioSubmission(DashboardModule): 47 | title = _("Radio") 48 | url = reverse("admin:forms_radiosheet_add") 49 | template = "gmmp/dashboard_modules/add_submission.html" 50 | 51 | def init_with_context(self, context): 52 | self.children = [ 53 | { 54 | "title": _("Code Story"), 55 | "url": reverse("admin:forms_radiosheet_add"), 56 | "description": _('''Code all the stories in the newscasts that you selected, including: All types of news — politics, local stories, international stories, reports on education, medicine, business, entertainment, and so on. 57 | Sports reports — code only if they are part of the newscast. (Do not code a programme if it is 58 | entirely about sports.)'''), 59 | }, 60 | ] 61 | 62 | 63 | class AddTelevisionSubmission(DashboardModule): 64 | title = _("Television") 65 | url = reverse("admin:forms_televisionsheet_add") 66 | template = "gmmp/dashboard_modules/add_submission.html" 67 | 68 | def init_with_context(self, context): 69 | self.children = [ 70 | { 71 | "title": _("Code Story"), 72 | "url": reverse("admin:forms_televisionsheet_add"), 73 | "description": _('''Code all the stories in the newscasts that you selected, including: All types of news — politics, local stories, international stories, reports on education, medicine, business, entertainment, and so on. 74 | Sports reports — code only if they are part of the newscast. (Do not code a programme if it is 75 | entirely about sports.)'''), 76 | }, 77 | ] 78 | 79 | 80 | class AddTwitterSubmission(DashboardModule): 81 | title = _("Twitter") 82 | url = reverse("admin:forms_twittersheet_add") 83 | template = "gmmp/dashboard_modules/add_submission.html" 84 | 85 | def init_with_context(self, context): 86 | self.children = [ 87 | { 88 | "title": _("Code Tweet"), 89 | "url": reverse("admin:forms_twittersheet_add"), 90 | "description": _('''Begin coding after 6.30 p.m. on the global monitoring day. Code every third tweet time stamped 6.30 p.m. or earlier published on the media monitoring day up to 15 – 20 tweets. If the Twitter news feed provider you have chosen does not yield 15 to 20 Tweets by taking every third item, take every second item. If they provide less than 15 tweets per day, they are not appropriate for inclusion in the monitoring.'''), 91 | }, 92 | ] 93 | 94 | 95 | class Submissions(DashboardModule): 96 | title = "Coded" 97 | template = "gmmp/dashboard_modules/submissions.html" 98 | 99 | def _get_count(self, model_, user): 100 | return model_.objects.count() if user.is_superuser else model_.objects.filter(country=user.monitor.country).count() 101 | 102 | def init_with_context(self, context): 103 | self.children = [ 104 | { 105 | "name": _("Newspapers"), 106 | "count": self._get_count(NewspaperSheet, context.request.user), 107 | "url": reverse("admin:forms_newspapersheet_changelist"), 108 | }, 109 | { 110 | "name": _("Radio"), 111 | "count": self._get_count(RadioSheet, context.request.user), 112 | "url": reverse("admin:forms_radiosheet_changelist"), 113 | }, 114 | { 115 | "name": _("Television"), 116 | "count": self._get_count(TelevisionSheet, context.request.user), 117 | "url": reverse("admin:forms_televisionsheet_changelist"), 118 | }, 119 | { 120 | "name": _("Internet"), 121 | "count": self._get_count(InternetNewsSheet, context.request.user), 122 | "url": reverse("admin:forms_internetnewssheet_changelist"), 123 | }, 124 | { 125 | "name": _("Twitter"), 126 | "count": self._get_count(TwitterSheet, context.request.user), 127 | "url": reverse("admin:forms_twittersheet_changelist"), 128 | }, 129 | ] 130 | -------------------------------------------------------------------------------- /gmmp/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2020-09-18 10:15+0000\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: gmmp/admin.py:13 22 | msgid "Monitor Details" 23 | msgstr "" 24 | 25 | #: gmmp/admin.py:17 26 | msgid "Country" 27 | msgstr "" 28 | 29 | #: gmmp/dashboard_modules.py:15 gmmp/dashboard_modules.py:117 30 | #: gmmp/settings.py:232 31 | msgid "Internet" 32 | msgstr "" 33 | 34 | #: gmmp/dashboard_modules.py:22 gmmp/dashboard_modules.py:38 35 | #: gmmp/dashboard_modules.py:54 gmmp/dashboard_modules.py:71 36 | msgid "Code Story" 37 | msgstr "" 38 | 39 | #: gmmp/dashboard_modules.py:24 40 | msgid "" 41 | "Code all online news content from the home page or 'first layer' of the site " 42 | "that are not designated as health/sports/entertainment/business news unless " 43 | "it is apparent that they are\n" 44 | "uncharacteristically important stories that day (i.e. would appear in the " 45 | "front page section of a newspaper instead of the appropriate sub-section)." 46 | msgstr "" 47 | 48 | #: gmmp/dashboard_modules.py:31 gmmp/dashboard_modules.py:102 49 | #: gmmp/settings.py:217 50 | msgid "Newspapers" 51 | msgstr "" 52 | 53 | #: gmmp/dashboard_modules.py:40 54 | msgid "" 55 | "Begin with the main news page (usually Page 1). Code all the news stories on " 56 | "this page. Then go to the next major news page.\n" 57 | "Code regular news stories only - not editorials, commentaries, letters to " 58 | "the editor." 59 | msgstr "" 60 | 61 | #: gmmp/dashboard_modules.py:47 gmmp/dashboard_modules.py:107 62 | #: gmmp/settings.py:222 63 | msgid "Radio" 64 | msgstr "" 65 | 66 | #: gmmp/dashboard_modules.py:56 gmmp/dashboard_modules.py:73 67 | msgid "" 68 | "Code all the stories in the newscasts that you selected, including: All " 69 | "types of news — politics, local stories, international stories, reports on " 70 | "education, medicine, business, entertainment, and so on.\n" 71 | "Sports reports — code only if they are part of the newscast. (Do not code a " 72 | "programme if it is\n" 73 | "entirely about sports.)" 74 | msgstr "" 75 | 76 | #: gmmp/dashboard_modules.py:64 gmmp/dashboard_modules.py:112 77 | #: gmmp/settings.py:227 78 | msgid "Television" 79 | msgstr "" 80 | 81 | #: gmmp/dashboard_modules.py:81 gmmp/dashboard_modules.py:122 82 | #: gmmp/settings.py:237 83 | msgid "Twitter" 84 | msgstr "" 85 | 86 | #: gmmp/dashboard_modules.py:88 87 | msgid "Code Tweet" 88 | msgstr "" 89 | 90 | #: gmmp/dashboard_modules.py:90 91 | msgid "" 92 | "Begin coding after 6.30 p.m. on the global monitoring day. Code every third " 93 | "tweet time stamped 6.30 p.m. or earlier published on the media monitoring " 94 | "day up to 15 – 20 tweets. If the Twitter news feed provider you have chosen " 95 | "does not yield 15 to 20 Tweets by taking every third item, take every second " 96 | "item. If they provide less than 15 tweets per day, they are not appropriate " 97 | "for inclusion in the monitoring." 98 | msgstr "" 99 | 100 | #: gmmp/settings.py:127 101 | msgid "English" 102 | msgstr "" 103 | 104 | #: gmmp/settings.py:128 105 | msgid "Spanish" 106 | msgstr "" 107 | 108 | #: gmmp/settings.py:129 109 | msgid "French" 110 | msgstr "" 111 | 112 | #: gmmp/settings.py:192 113 | msgid "Belgium - French" 114 | msgstr "" 115 | 116 | #: gmmp/settings.py:193 117 | msgid "Belgium - Flemish" 118 | msgstr "" 119 | 120 | #: gmmp/settings.py:195 121 | msgid "England" 122 | msgstr "" 123 | 124 | #: gmmp/settings.py:196 125 | msgid "North Ireland" 126 | msgstr "" 127 | 128 | #: gmmp/settings.py:197 129 | msgid "Scotland" 130 | msgstr "" 131 | 132 | #: gmmp/settings.py:198 133 | msgid "Wales" 134 | msgstr "" 135 | 136 | #: gmmp/settings.py:199 137 | msgid "International" 138 | msgstr "" 139 | 140 | #: gmmp/settings.py:203 141 | msgid "GMMP Database" 142 | msgstr "" 143 | 144 | #: gmmp/settings.py:214 145 | msgid "Code" 146 | msgstr "" 147 | 148 | #: gmmp/settings.py:244 149 | msgid "Coded" 150 | msgstr "" 151 | 152 | #: gmmp/settings.py:254 153 | msgid "Access Control" 154 | msgstr "" 155 | 156 | #: gmmp/settings.py:258 157 | msgid "General" 158 | msgstr "" 159 | 160 | #: gmmp/settings.py:261 161 | msgid "Help" 162 | msgstr "" 163 | 164 | #: gmmp/settings.py:263 165 | msgid "Facebook Group" 166 | msgstr "" 167 | 168 | #: gmmp/settings.py:268 169 | msgid "Methodology Guides & Coding Tools" 170 | msgstr "" 171 | 172 | #: gmmp/settings.py:275 173 | msgid "Other Links" 174 | msgstr "" 175 | 176 | #: gmmp/settings.py:278 177 | msgid "Who Makes The News" 178 | msgstr "" 179 | 180 | #: gmmp/settings.py:283 181 | msgid "Facebook" 182 | msgstr "" 183 | 184 | #: gmmp/templates/admin/base_site.html:41 185 | msgid "Welcome," 186 | msgstr "" 187 | 188 | #: gmmp/templates/admin/change_form.html:6 189 | msgid "Home" 190 | msgstr "" 191 | 192 | #: gmmp/templates/admin/change_form.html:9 193 | msgid "Add Article" 194 | msgstr "" 195 | 196 | #: gmmp/templates/admin/login.html:23 197 | msgid "Please correct the error below." 198 | msgstr "" 199 | 200 | #: gmmp/templates/admin/login.html:23 201 | msgid "Please correct the errors below." 202 | msgstr "" 203 | 204 | #: gmmp/templates/admin/login.html:50 205 | msgid "Forgotten your password or username?" 206 | msgstr "" 207 | 208 | #: gmmp/templates/admin/login.html:54 209 | msgid "Log in" 210 | msgstr "" 211 | 212 | #: gmmp/templates/admin/submit_line.html:9 213 | msgid "Delete" 214 | msgstr "" 215 | 216 | #: gmmp/templates/admin/submit_line.html:11 217 | msgid "Save as new" 218 | msgstr "" 219 | 220 | #: gmmp/templates/admin/submit_line.html:12 221 | msgid "Save and continue editing" 222 | msgstr "" 223 | 224 | #: gmmp/templates/admin/submit_line.html:12 225 | msgid "Save and view" 226 | msgstr "" 227 | 228 | #: gmmp/templates/admin/submit_line.html:13 229 | msgid "Save and add another" 230 | msgstr "" 231 | 232 | #: gmmp/templates/admin/submit_line.html:14 233 | msgid "Save" 234 | msgstr "" 235 | 236 | #: gmmp/templates/admin/submit_line.html:15 237 | msgid "Close" 238 | msgstr "" 239 | -------------------------------------------------------------------------------- /forms/migrations/0020_auto_20150506_1136.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django_countries.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('gmmp', '0001_initial'), 12 | ('forms', '0019_auto_20150506_1052'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='internetnewssheet', 18 | name='country', 19 | field=django_countries.fields.CountryField(max_length=2, null=True), 20 | preserve_default=True, 21 | ), 22 | migrations.AddField( 23 | model_name='internetnewssheet', 24 | name='monitor', 25 | field=models.ForeignKey(to='gmmp.Monitor', null=True, on_delete=models.SET_NULL), 26 | preserve_default=True, 27 | ), 28 | migrations.AddField( 29 | model_name='newspapersheet', 30 | name='country', 31 | field=django_countries.fields.CountryField(max_length=2, null=True), 32 | preserve_default=True, 33 | ), 34 | migrations.AddField( 35 | model_name='newspapersheet', 36 | name='monitor', 37 | field=models.ForeignKey(to='gmmp.Monitor', null=True, on_delete=models.SET_NULL), 38 | preserve_default=True, 39 | ), 40 | migrations.AddField( 41 | model_name='radiosheet', 42 | name='country', 43 | field=django_countries.fields.CountryField(max_length=2, null=True), 44 | preserve_default=True, 45 | ), 46 | migrations.AddField( 47 | model_name='radiosheet', 48 | name='monitor', 49 | field=models.ForeignKey(to='gmmp.Monitor', null=True, on_delete=models.SET_NULL), 50 | preserve_default=True, 51 | ), 52 | migrations.AddField( 53 | model_name='televisionsheet', 54 | name='country', 55 | field=django_countries.fields.CountryField(max_length=2, null=True), 56 | preserve_default=True, 57 | ), 58 | migrations.AddField( 59 | model_name='televisionsheet', 60 | name='monitor', 61 | field=models.ForeignKey(to='gmmp.Monitor', null=True, on_delete=models.SET_NULL), 62 | preserve_default=True, 63 | ), 64 | migrations.AddField( 65 | model_name='twittersheet', 66 | name='country', 67 | field=django_countries.fields.CountryField(max_length=2, null=True), 68 | preserve_default=True, 69 | ), 70 | migrations.AddField( 71 | model_name='twittersheet', 72 | name='monitor', 73 | field=models.ForeignKey(to='gmmp.Monitor', null=True, on_delete=models.SET_NULL), 74 | preserve_default=True, 75 | ), 76 | 77 | migrations.RunSQL( 78 | """ 79 | update forms_internetnewssheet 80 | 81 | set country = monitor.country, 82 | monitor_id = monitor.id 83 | 84 | from 85 | forms_internetnewssheet as sheet 86 | inner join 87 | (select distinct object_pk, content_type_id, user_id from guardian_userobjectpermission) as perms on perms.object_pk = cast(sheet.id as varchar) 88 | inner join django_content_type on django_content_type.model = 'internetnewssheet' and perms.content_type_id = django_content_type.id 89 | inner join gmmp_monitor monitor on monitor.user_id = perms.user_id 90 | 91 | where forms_internetnewssheet.id = sheet.id 92 | ; 93 | 94 | update forms_newspapersheet 95 | 96 | set country = monitor.country, 97 | monitor_id = monitor.id 98 | 99 | from 100 | forms_newspapersheet as sheet 101 | inner join 102 | (select distinct object_pk, content_type_id, user_id from guardian_userobjectpermission) as perms on perms.object_pk = cast(sheet.id as varchar) 103 | inner join django_content_type on django_content_type.model = 'newspapersheet' and perms.content_type_id = django_content_type.id 104 | inner join gmmp_monitor monitor on monitor.user_id = perms.user_id 105 | 106 | where forms_newspapersheet.id = sheet.id 107 | ; 108 | 109 | update forms_televisionsheet 110 | 111 | set country = monitor.country, 112 | monitor_id = monitor.id 113 | 114 | from 115 | forms_televisionsheet as sheet 116 | inner join 117 | (select distinct object_pk, content_type_id, user_id from guardian_userobjectpermission) as perms on perms.object_pk = cast(sheet.id as varchar) 118 | inner join django_content_type on django_content_type.model = 'televisionsheet' and perms.content_type_id = django_content_type.id 119 | inner join gmmp_monitor monitor on monitor.user_id = perms.user_id 120 | 121 | where forms_televisionsheet.id = sheet.id 122 | ; 123 | 124 | update forms_radiosheet 125 | 126 | set country = monitor.country, 127 | monitor_id = monitor.id 128 | 129 | from 130 | forms_radiosheet as sheet 131 | inner join 132 | (select distinct object_pk, content_type_id, user_id from guardian_userobjectpermission) as perms on perms.object_pk = cast(sheet.id as varchar) 133 | inner join django_content_type on django_content_type.model = 'radiosheet' and perms.content_type_id = django_content_type.id 134 | inner join gmmp_monitor monitor on monitor.user_id = perms.user_id 135 | 136 | where forms_radiosheet.id = sheet.id 137 | ; 138 | 139 | update forms_twittersheet 140 | 141 | set country = monitor.country, 142 | monitor_id = monitor.id 143 | 144 | from 145 | forms_twittersheet as sheet 146 | inner join 147 | (select distinct object_pk, content_type_id, user_id from guardian_userobjectpermission) as perms on perms.object_pk = cast(sheet.id as varchar) 148 | inner join django_content_type on django_content_type.model = 'twittersheet' and perms.content_type_id = django_content_type.id 149 | inner join gmmp_monitor monitor on monitor.user_id = perms.user_id 150 | 151 | where forms_twittersheet.id = sheet.id 152 | ; 153 | """, 154 | """ 155 | select 1; 156 | """ 157 | ) 158 | ] 159 | --------------------------------------------------------------------------------