Skip to content

Commit

Permalink
Merge pull request #426 from project-icp/jf/export-csv-fxn
Browse files Browse the repository at this point in the history
Export all survey tables to CSV
  • Loading branch information
fungjj92 authored Jan 24, 2019
2 parents 0ccd132 + e371fce commit 6204798
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 20 deletions.
10 changes: 10 additions & 0 deletions src/icp/apps/beekeepers/js/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,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.',
Expand All @@ -181,6 +182,7 @@ export function signUp(form) {
dispatch(setAuthState({
username: '',
authError: error.response.data.errors[0],
isStaff: false,
userId: null,
userSurvey: null,
message: '',
Expand All @@ -201,6 +203,7 @@ export function login(form) {
username: data.username || '',
authError: '',
message: '',
isStaff: data.is_staff,
userId: data.id || null,
userSurvey: data.beekeeper_survey,
}));
Expand All @@ -210,6 +213,7 @@ export function login(form) {
dispatch(setAuthState({
username: '',
message: '',
isStaff: false,
authError: error.response.data.errors[0],
userId: null,
userSurvey: null,
Expand All @@ -224,6 +228,7 @@ export function logout() {
username: '',
authError: '',
message: '',
isStaff: false,
userId: null,
userSurvey: null,
}));
Expand All @@ -240,13 +245,15 @@ 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,
}));
}).catch((error) => {
dispatch(setAuthState({
message: '',
authError: error.response.data.errors[0],
isStaff: false,
username: '',
userSurvey: null,
userId: null,
Expand All @@ -260,6 +267,7 @@ export function createUserSurvey(form) {
auth: {
username,
userId,
isStaff,
},
} = getState();

Expand All @@ -270,6 +278,7 @@ export function createUserSurvey(form) {
message: '',
authError: '',
userId,
isStaff,
userSurvey: data.beekeeper_survey,
}));
dispatch(closeUserSurveyModal());
Expand All @@ -281,6 +290,7 @@ export function createUserSurvey(form) {
username,
message: '',
authError: errorMsg,
isStaff,
userId,
userSurvey: null,
}));
Expand Down
62 changes: 44 additions & 18 deletions src/icp/apps/beekeepers/js/src/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -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 => (
<a
rel="noopener noreferrer"
className={`navbar__link ${srOnly ? 'sr-only' : ''}`}
href="/beekeepers/export/"
>
Export Survey Data
</a>
);

const exportDataButton = isStaff
? (
<li>
{makeExportDataButton()}
</li>
) : null;

const exportDataButtonForScreenReader = isStaff
? makeExportDataButton(true) : null;

const makeLogOutButton = srOnly => (
<button
type="button"
className={`navbar__button ${srOnly ? 'sr-only' : ''}`}
onClick={() => dispatch(logout())}
>
Log Out
</button>
);

const Header = ({ dispatch, username }) => {
const authButtons = username
? (
<li className="navbar__item navbar__item--user">
Expand All @@ -16,23 +50,13 @@ const Header = ({ dispatch, username }) => {
{username}
</button>
{/* Hidden Log Out button for screen readers */}
<button
type="button"
className="sr-only"
onClick={() => dispatch(logout())}
>
Log Out
</button>
{/* Hidden buttons for screen readers */}
{exportDataButtonForScreenReader}
{makeLogOutButton(true)}
<ul className="navbar__options">
{exportDataButton}
<li>
<button
type="button"
className="navbar__button"
onClick={() => dispatch(logout())}
>
Log Out
</button>
{makeLogOutButton()}
</li>
</ul>
</li>
Expand All @@ -59,6 +83,7 @@ const Header = ({ dispatch, username }) => {
</li>
</>
);

return (
<header className="header">
<div className="navbar">
Expand Down Expand Up @@ -92,6 +117,7 @@ function mapStateToProps(state) {
Header.propTypes = {
dispatch: func.isRequired,
username: string.isRequired,
isStaff: bool.isRequired,
};

export default withRouter(connect(mapStateToProps)(Header));
1 change: 1 addition & 0 deletions src/icp/apps/beekeepers/js/src/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const initialAuthState = {
userId: null,
authError: '',
message: '',
isStaff: false,
userSurvey: null,
};

Expand Down
5 changes: 5 additions & 0 deletions src/icp/apps/beekeepers/sass/06_components/_navbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
}
}

&__link {
color: $color-grey-2;
text-decoration: none;
}

&__button {
padding: 0;
color: $color-grey-2;
Expand Down
1 change: 1 addition & 0 deletions src/icp/apps/beekeepers/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<apiary_id>[0-9]+)/survey/$',
views.create_survey, name='survey-create'),
Expand Down
68 changes: 66 additions & 2 deletions src/icp/apps/beekeepers/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'])
Expand Down
2 changes: 2 additions & 0 deletions src/icp/apps/user/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
}
Expand Down

0 comments on commit 6204798

Please sign in to comment.