Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export all survey tables to CSV #426

Merged
merged 7 commits into from
Jan 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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.',
Expand All @@ -179,6 +180,7 @@ export function signUp(form) {
dispatch(setAuthState({
username: '',
authError: error.response.data.errors[0],
isStaff: false,
userId: null,
userSurvey: null,
message: '',
Expand All @@ -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,
}));
Expand All @@ -208,6 +211,7 @@ export function login(form) {
dispatch(setAuthState({
username: '',
message: '',
isStaff: false,
authError: error.response.data.errors[0],
userId: null,
userSurvey: null,
Expand All @@ -222,6 +226,7 @@ export function logout() {
username: '',
authError: '',
message: '',
isStaff: false,
userId: null,
userSurvey: null,
}));
Expand All @@ -238,13 +243,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 @@ -258,6 +265,7 @@ export function createUserSurvey(form) {
auth: {
username,
userId,
isStaff,
},
} = getState();

Expand All @@ -268,6 +276,7 @@ export function createUserSurvey(form) {
message: '',
authError: '',
userId,
isStaff,
userSurvey: data.beekeeper_survey,
}));
dispatch(closeUserSurveyModal());
Expand All @@ -279,6 +288,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)}
fungjj92 marked this conversation as resolved.
Show resolved Hide resolved
<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 @@ -89,6 +89,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']

fungjj92 marked this conversation as resolved.
Show resolved Hide resolved

@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()
rajadain marked this conversation as resolved.
Show resolved Hide resolved
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