diff --git a/src/icp/apps/beekeepers/js/src/actions.js b/src/icp/apps/beekeepers/js/src/actions.js index addfc86e5..d0395cd63 100644 --- a/src/icp/apps/beekeepers/js/src/actions.js +++ b/src/icp/apps/beekeepers/js/src/actions.js @@ -168,6 +168,7 @@ export function signUp(form) { dispatch(setAuthState({ username: '', authError: '', + isStaff: false, userId: null, userSurvey: null, message: 'Please click the validation link in your email and then log in.', @@ -179,6 +180,7 @@ export function signUp(form) { dispatch(setAuthState({ username: '', authError: error.response.data.errors[0], + isStaff: false, userId: null, userSurvey: null, message: '', @@ -199,6 +201,7 @@ export function login(form) { username: data.username || '', authError: '', message: '', + isStaff: data.is_staff, userId: data.id || null, userSurvey: data.beekeeper_survey, })); @@ -208,6 +211,7 @@ export function login(form) { dispatch(setAuthState({ username: '', message: '', + isStaff: false, authError: error.response.data.errors[0], userId: null, userSurvey: null, @@ -222,6 +226,7 @@ export function logout() { username: '', authError: '', message: '', + isStaff: false, userId: null, userSurvey: null, })); @@ -238,6 +243,7 @@ export function sendAuthLink(form, endpoint) { username: '', userId: null, message: 'Check your email to reset your password or activate your account', + isStaff: false, authError: '', userSurvey: null, })); @@ -245,6 +251,7 @@ export function sendAuthLink(form, endpoint) { dispatch(setAuthState({ message: '', authError: error.response.data.errors[0], + isStaff: false, username: '', userSurvey: null, userId: null, @@ -258,6 +265,7 @@ export function createUserSurvey(form) { auth: { username, userId, + isStaff, }, } = getState(); @@ -268,6 +276,7 @@ export function createUserSurvey(form) { message: '', authError: '', userId, + isStaff, userSurvey: data.beekeeper_survey, })); dispatch(closeUserSurveyModal()); @@ -279,6 +288,7 @@ export function createUserSurvey(form) { username, message: '', authError: errorMsg, + isStaff, userId, userSurvey: null, })); diff --git a/src/icp/apps/beekeepers/js/src/components/Header.jsx b/src/icp/apps/beekeepers/js/src/components/Header.jsx index 4e1e639f4..c92274f02 100644 --- a/src/icp/apps/beekeepers/js/src/components/Header.jsx +++ b/src/icp/apps/beekeepers/js/src/components/Header.jsx @@ -1,11 +1,45 @@ import React from 'react'; import { connect } from 'react-redux'; import { NavLink, withRouter } from 'react-router-dom'; -import { func, string } from 'prop-types'; +import { func, string, bool } from 'prop-types'; -import { openParticipateModal, openLoginModal, logout } from '../actions'; +import { + openParticipateModal, + openLoginModal, + logout, +} from '../actions'; + +const Header = ({ dispatch, username, isStaff }) => { + const makeExportDataButton = srOnly => ( + + Export Survey Data + + ); + + const exportDataButton = isStaff + ? ( +
  • + {makeExportDataButton()} +
  • + ) : null; + + const exportDataButtonForScreenReader = isStaff + ? makeExportDataButton(true) : null; + + const makeLogOutButton = srOnly => ( + + ); -const Header = ({ dispatch, username }) => { const authButtons = username ? (
  • @@ -16,23 +50,13 @@ const Header = ({ dispatch, username }) => { {username} ▾ - {/* Hidden Log Out button for screen readers */} - + {/* Hidden buttons for screen readers */} + {exportDataButtonForScreenReader} + {makeLogOutButton(true)}
  • @@ -59,6 +83,7 @@ const Header = ({ dispatch, username }) => { ); + return (
    @@ -92,6 +117,7 @@ function mapStateToProps(state) { Header.propTypes = { dispatch: func.isRequired, username: string.isRequired, + isStaff: bool.isRequired, }; export default withRouter(connect(mapStateToProps)(Header)); diff --git a/src/icp/apps/beekeepers/js/src/reducers.js b/src/icp/apps/beekeepers/js/src/reducers.js index 11692e4b8..68fd81a06 100644 --- a/src/icp/apps/beekeepers/js/src/reducers.js +++ b/src/icp/apps/beekeepers/js/src/reducers.js @@ -89,6 +89,7 @@ const initialAuthState = { userId: null, authError: '', message: '', + isStaff: false, userSurvey: null, }; diff --git a/src/icp/apps/beekeepers/sass/06_components/_navbar.scss b/src/icp/apps/beekeepers/sass/06_components/_navbar.scss index 963ba067e..fad9b5d4f 100644 --- a/src/icp/apps/beekeepers/sass/06_components/_navbar.scss +++ b/src/icp/apps/beekeepers/sass/06_components/_navbar.scss @@ -47,6 +47,11 @@ } } + &__link { + color: $color-grey-2; + text-decoration: none; + } + &__button { padding: 0; color: $color-grey-2; diff --git a/src/icp/apps/beekeepers/urls.py b/src/icp/apps/beekeepers/urls.py index ea0493f32..41ed6be98 100644 --- a/src/icp/apps/beekeepers/urls.py +++ b/src/icp/apps/beekeepers/urls.py @@ -16,6 +16,7 @@ urlpatterns = patterns( '', + url(r'export/$', views.export_survey_tables, name='export'), url(r'fetch/$', views.fetch_data, name='fetch_data'), url(r'^apiary/(?P[0-9]+)/survey/$', views.create_survey, name='survey-create'), diff --git a/src/icp/apps/beekeepers/views.py b/src/icp/apps/beekeepers/views.py index 02e3c674c..ad24f6be8 100644 --- a/src/icp/apps/beekeepers/views.py +++ b/src/icp/apps/beekeepers/views.py @@ -2,14 +2,19 @@ from __future__ import division import os +import psycopg2 +from cStringIO import StringIO +from zipfile import ZipFile +from tempfile import SpooledTemporaryFile -from django.http import Http404 +from django.conf import settings +from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404 from django.utils.timezone import now from rest_framework import decorators, viewsets from rest_framework.response import Response -from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser from rest_framework.status import HTTP_204_NO_CONTENT from models import Apiary, Survey, UserSurvey @@ -22,6 +27,65 @@ DATA_BUCKET = os.environ['AWS_BEEKEEPERS_DATA_BUCKET'] +PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) +DB = settings.DATABASES['default'] + + +@decorators.api_view(['GET']) +@decorators.permission_classes((IsAdminUser, )) +def export_survey_tables(request): + """Export a zip file of CSVs of all types of beekeepers surveys.""" + + # prepare queries to the database + db_options = 'dbname={} user={} host={} password={}'.format( + DB['NAME'], DB['USER'], DB['HOST'], DB['PASSWORD'] + ) + connection = psycopg2.connect(db_options) + cur = connection.cursor() + tables = dict( + novembersurvey=None, + aprilsurvey=None, + monthlysurvey=None, + usersurvey=""" + SELECT auth_user.username, auth_user.email, + beekeepers_usersurvey.* + FROM beekeepers_usersurvey + INNER JOIN auth_user ON beekeepers_usersurvey.user_id=auth_user.id + """, + survey=""" + SELECT beekeepers_survey.*, beekeepers_apiary.lat, + beekeepers_apiary.lng + FROM beekeepers_survey + INNER JOIN beekeepers_apiary + ON beekeepers_survey.apiary_id=beekeepers_apiary.id + """, + ) + + # the zipped CSVs are written in memory + date_stamp = now().strftime('%Y-%m-%d_%H-%M-%S') + zip_dir = 'beekeepers_exports_{}'.format(date_stamp) + stream = StringIO() + + with ZipFile(stream, 'w') as zf: + for table, query in tables.iteritems(): + if query is None: + query = 'SELECT * FROM beekeepers_{}'.format(table) + + filename = '{}/{}_{}.csv'.format(zip_dir, table, date_stamp) + full_query = 'COPY ({0}) TO STDOUT WITH CSV HEADER'.format(query) + tempfile = SpooledTemporaryFile() + cur.copy_expert(full_query, tempfile) + tempfile.seek(0) + zf.writestr(filename, tempfile.read()) + zf.close() + + resp = HttpResponse( + stream.getvalue(), + content_type='application/zip' + ) + resp['Content-Disposition'] = 'attachment; filename={}.zip'.format(zip_dir) + connection.close() + return resp @decorators.api_view(['POST']) diff --git a/src/icp/apps/user/views.py b/src/icp/apps/user/views.py index a8866bdf0..baf4cd719 100644 --- a/src/icp/apps/user/views.py +++ b/src/icp/apps/user/views.py @@ -48,6 +48,7 @@ def login(request): 'username': user.username, 'guest': False, 'id': user.id, + 'is_staff': user.is_staff, 'beekeeper_survey': user_survey } else: @@ -79,6 +80,7 @@ def login(request): 'result': 'success', 'username': user.username, 'guest': False, + 'is_staff': user.is_staff, 'id': user.id, 'beekeeper_survey': user_survey }