├── crm
├── __init__.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ ├── make_superuser.py
│ │ └── dedupe.py
├── migrations
│ ├── __init__.py
│ ├── 0016_merge_20190508_0013.py
│ ├── 0017_remove_person_address.py
│ ├── 0013_remove_turfmembership_is_captain.py
│ ├── 0011_person_phone.py
│ ├── 0015_auto_20190507_2326.py
│ ├── 0010_auto_20180820_0223.py
│ ├── 0018_auto_20190510_2349.py
│ ├── 0006_auto_20180802_0712.py
│ ├── 0012_person_location.py
│ ├── 0009_auto_20180807_0304.py
│ ├── 0012_person_is_captain.py
│ ├── 0014_auto_20190507_2318.py
│ ├── 0008_auto_20180806_0008.py
│ ├── 0002_auto_20180729_2011_squashed_0005_auto_20180729_2125.py
│ ├── 0001_initial.py
│ ├── 0007_auto_20180805_2335.py
│ ├── 0013_convert_to_geocodable.py
│ └── 0003_auto_20180802_0200_squashed_0005_auto_20180802_0319.py
├── templates
│ ├── index.html
│ ├── robots.txt
│ ├── email.eml
│ ├── action.html
│ ├── form.html
│ └── report.html
├── apps.py
├── urls.py
├── pipeline.py
├── context.py
├── views.py
└── exporting.py
├── events
├── __init__.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ ├── list_members.py
│ │ └── leaderboard.py
├── migrations
│ ├── __init__.py
│ ├── 0006_remove_event_address.py
│ ├── 0005_auto_20190510_2349.py
│ ├── 0003_auto_20190510_2346.py
│ ├── 0002_auto_20180824_2228.py
│ ├── 0004_event_location.py
│ └── 0001_initial.py
├── apps.py
├── api_views.py
├── tests.py
├── admin.py
├── serializers.py
└── models.py
├── sync
├── __init__.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ ├── export.py
│ │ └── import.py
├── migrations
│ ├── __init__.py
│ ├── 0004_auto_20190731_1553.py
│ ├── 0003_synctarget_lastrun.py
│ ├── 0002_auto_20190730_1850.py
│ └── 0001_initial.py
├── tests.py
├── views.py
├── apps.py
├── templates
│ └── admin
│ │ └── sync
│ │ ├── add.html
│ │ └── change_form.html
├── forms.py
└── models.py
├── donations
├── __init__.py
├── migrations
│ ├── __init__.py
│ ├── 0002_auto_20181021_0226.py
│ └── 0001_initial.py
├── tests.py
├── apps.py
├── models.py
├── admin.py
└── importing.py
├── filtering
├── __init__.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── query.py
├── migrations
│ ├── __init__.py
│ └── 0001_initial.py
├── apps.py
└── admin.py
├── geocodable
├── __init__.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── resolve_all.py
├── migrations
│ ├── __init__.py
│ └── 0001_initial.py
├── views.py
├── apps.py
├── admin.py
├── api_views.py
├── serializers.py
└── api.py
├── onboarding
├── __init__.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ ├── new_neighbors.py
│ │ └── onboard.py
├── migrations
│ ├── __init__.py
│ ├── 0008_remove_signup_deleted.py
│ ├── 0006_auto_20180814_1829.py
│ ├── 0011_auto_20181023_0619.py
│ ├── 0002_newneighbornotificationtarget_last_notified.py
│ ├── 0013_auto_20190402_2358.py
│ ├── 0010_auto_20181023_0617.py
│ ├── 0014_auto_20190507_2318.py
│ ├── 0004_auto_20180810_0755.py
│ ├── 0003_newneighbornotificationtarget_state.py
│ ├── 0005_auto_20180810_0756.py
│ ├── 0001_initial.py
│ ├── 0012_auto_20181107_2352.py
│ ├── 0007_auto_20181016_0515.py
│ └── 0009_auto_20181021_2234.py
├── apps.py
├── templates
│ └── new-neighbors-notification.eml
├── api_views.py
├── serializers.py
├── jobs.py
├── admin.py
└── models.py
├── organizer
├── __init__.py
├── wsgi.py
├── pipeline.py
├── viewsets.py
├── exporting.py
└── importing.py
├── assets
├── .gitignore
├── img
│ ├── bg.png
│ ├── logo.png
│ ├── logo-wide.png
│ ├── unfurl-bg.png
│ ├── logo-square.png
│ └── symbol.svg
├── js
│ ├── style-bootstrap.js
│ ├── store
│ │ ├── history.js
│ │ ├── provider.js
│ │ ├── index.js
│ │ ├── store.js
│ │ ├── filter.js
│ │ ├── select.js
│ │ └── model.test.js
│ ├── selectors
│ │ ├── auth.js
│ │ ├── index.js
│ │ ├── geocache.js
│ │ ├── people.js
│ │ └── events.test.js
│ ├── setupTests.js
│ ├── components
│ │ ├── ImportDialog.test.js
│ │ ├── App.test.js
│ │ ├── MapIndex.test.js
│ │ ├── MapIndex.scss
│ │ ├── chrome
│ │ │ ├── Logo.js
│ │ │ ├── LoadingSpinner.js
│ │ │ ├── BusyIndicator.js
│ │ │ ├── BusyIndicator.test.js
│ │ │ ├── LoginButtons.test.js
│ │ │ ├── OrganizerAppBar.test.js
│ │ │ ├── OrganizerBottomNav.test.js
│ │ │ ├── LoginButtons.js
│ │ │ └── OrganizerBottomNav.js
│ │ ├── PeopleIndex.test.js
│ │ ├── Gravatar.js
│ │ ├── mapping
│ │ │ ├── LocalMap.js
│ │ │ ├── MarkerMap.js
│ │ │ ├── BaseMap.js
│ │ │ └── LocatorControl.js
│ │ ├── UserAvatar.js
│ │ ├── people-browser
│ │ │ ├── BooleanSelect.js
│ │ │ ├── index.test.js
│ │ │ └── Search.js
│ │ ├── MaterialFormSwitch.test.js
│ │ ├── MaterialFormSwitch.js
│ │ ├── MaterialFormText.test.js
│ │ ├── AppRoutes.test.js
│ │ ├── MaterialFormSelect.test.js
│ │ ├── events
│ │ │ ├── NoEvents.js
│ │ │ └── NoLocation.js
│ │ ├── activities
│ │ │ └── checkin
│ │ │ │ ├── Skeleton.js
│ │ │ │ └── SignupForm.js
│ │ ├── MaterialFormSelect.js
│ │ ├── MaterialFormText.js
│ │ ├── MaterialFormModelSelect.js
│ │ ├── AppRoutes.js
│ │ ├── ErrorWrapper.js
│ │ ├── App.js
│ │ └── MapIndex.js
│ ├── Django.js
│ ├── reducers
│ │ ├── index.js
│ │ ├── filters.js
│ │ ├── auth.js
│ │ ├── geocache.js
│ │ ├── model.js
│ │ ├── select.js
│ │ └── model.test.js
│ ├── Breakpoint.js
│ ├── actions
│ │ ├── index.js
│ │ ├── index.test.js
│ │ └── geocache.js
│ ├── index.test.js
│ ├── index.js
│ └── lib
│ │ └── filter-ast.js
├── manifest.json
├── scss
│ ├── app.scss
│ ├── spinner.scss
│ └── form-view.scss
└── index.html
├── __mocks__
├── styleMock.js
├── fileMock.js
├── raven-js.js
└── history.js
├── Procfile
├── .coveragerc
├── .gitignore
├── Dockerfile
├── pytest.ini
├── docs
├── usage.rst
├── import-export.rst
├── events.rst
├── broadcasts.rst
├── maintenance.rst
├── crm.rst
├── installation.rst
├── integrations.rst
└── development.rst
├── static
└── app.css
├── docker-compose.yml
├── .github
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── manage.py
├── webpack.prod.js
├── webpack.dev.js
├── .eslintrc.js
├── README.md
├── index.rst
├── Pipfile
└── app.json
/crm/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/events/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sync/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/donations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/filtering/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/geocodable/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/onboarding/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/organizer/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/crm/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/crm/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sync/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sync/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/.gitignore:
--------------------------------------------------------------------------------
1 | bundles
2 |
--------------------------------------------------------------------------------
/donations/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/events/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/events/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/filtering/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/filtering/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/geocodable/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/geocodable/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/onboarding/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/onboarding/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/crm/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/events/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sync/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/filtering/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/geocodable/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/onboarding/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/__mocks__/styleMock.js:
--------------------------------------------------------------------------------
1 | module.exports = {}
2 |
--------------------------------------------------------------------------------
/__mocks__/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = "test-stub"
2 |
--------------------------------------------------------------------------------
/crm/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'layout.html' %}
2 |
--------------------------------------------------------------------------------
/crm/templates/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: *
3 |
--------------------------------------------------------------------------------
/assets/img/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tdfischer/organizer/HEAD/assets/img/bg.png
--------------------------------------------------------------------------------
/assets/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tdfischer/organizer/HEAD/assets/img/logo.png
--------------------------------------------------------------------------------
/crm/templates/email.eml:
--------------------------------------------------------------------------------
1 | {{body}}
2 | --------
3 | This email was sent with EBF Organizer.
4 |
--------------------------------------------------------------------------------
/assets/js/style-bootstrap.js:
--------------------------------------------------------------------------------
1 | import { install } from '@material-ui/styles'
2 |
3 | install()
4 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: gunicorn organizer.wsgi --preload --log-file -
2 | worker: python manage.py rqworker
3 |
--------------------------------------------------------------------------------
/assets/img/logo-wide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tdfischer/organizer/HEAD/assets/img/logo-wide.png
--------------------------------------------------------------------------------
/assets/img/unfurl-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tdfischer/organizer/HEAD/assets/img/unfurl-bg.png
--------------------------------------------------------------------------------
/assets/img/logo-square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tdfischer/organizer/HEAD/assets/img/logo-square.png
--------------------------------------------------------------------------------
/__mocks__/raven-js.js:
--------------------------------------------------------------------------------
1 | module.exports = jest.genMockFromModule('raven-js')
2 | module.exports.config = jest.fn(() => ({install:jest.fn()}))
3 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | omit =
3 | */apps.py
4 | */management/commands/*
5 | manage.py
6 | conf.py
7 | organizer/wsgi.py
8 | organizer/settings.py
9 |
--------------------------------------------------------------------------------
/assets/js/store/history.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history'
2 |
3 | export const history = createBrowserHistory()
4 | export default history
5 |
--------------------------------------------------------------------------------
/sync/tests.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.test import TestCase
5 |
6 | # Create your tests here.
7 |
--------------------------------------------------------------------------------
/donations/tests.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.test import TestCase
5 |
6 | # Create your tests here.
7 |
--------------------------------------------------------------------------------
/sync/views.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.shortcuts import render
5 |
6 | # Create your views here.
7 |
--------------------------------------------------------------------------------
/assets/js/selectors/auth.js:
--------------------------------------------------------------------------------
1 | export const getCurrentUser = state => state.getIn(['auth', 'user'], {})
2 | export const getLoggedIn = state => !!getCurrentUser(state).id
3 |
--------------------------------------------------------------------------------
/geocodable/views.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.shortcuts import render
5 |
6 | # Create your views here.
7 |
--------------------------------------------------------------------------------
/crm/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.apps import AppConfig
5 |
6 |
7 | class CrmConfig(AppConfig):
8 | name = 'crm'
9 |
--------------------------------------------------------------------------------
/sync/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.apps import AppConfig
5 |
6 |
7 | class SyncConfig(AppConfig):
8 | name = 'sync'
9 |
--------------------------------------------------------------------------------
/events/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.apps import AppConfig
5 |
6 |
7 | class EventsConfig(AppConfig):
8 | name = 'events'
9 |
--------------------------------------------------------------------------------
/donations/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.apps import AppConfig
5 |
6 |
7 | class DonationsConfig(AppConfig):
8 | name = 'donations'
9 |
--------------------------------------------------------------------------------
/filtering/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.apps import AppConfig
5 |
6 |
7 | class FilteringConfig(AppConfig):
8 | name = 'filtering'
9 |
--------------------------------------------------------------------------------
/geocodable/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.apps import AppConfig
5 |
6 |
7 | class GeocodableConfig(AppConfig):
8 | name = 'geocodable'
9 |
--------------------------------------------------------------------------------
/onboarding/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.apps import AppConfig
5 |
6 |
7 | class OnboardingConfig(AppConfig):
8 | name = 'onboarding'
9 |
--------------------------------------------------------------------------------
/assets/js/setupTests.js:
--------------------------------------------------------------------------------
1 | import { configure } from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 | import geolocate from 'mock-geolocation'
4 |
5 | geolocate.use()
6 | configure({ adapter: new Adapter() });
7 |
--------------------------------------------------------------------------------
/__mocks__/history.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | createBrowserHistory: () => {
3 | return {
4 | location: {},
5 | listen: jest.fn(),
6 | action: "",
7 | push: jest.fn()
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/crm/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url, include
2 | from . import views
3 |
4 | urlpatterns = [
5 | url(r'^service-worker.js', views.service_worker),
6 | url(r'^robots.txt', views.robots),
7 | url(r'^', views.index),
8 | ]
9 |
--------------------------------------------------------------------------------
/assets/js/components/ImportDialog.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import ImportDialog from './ImportDialog'
4 |
5 | it('should render defaults safely', () => {
6 | shallow()
7 | })
8 |
9 |
--------------------------------------------------------------------------------
/geocodable/admin.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.contrib import admin
5 | from . import models
6 |
7 | admin.site.register(models.Location)
8 | admin.site.register(models.LocationAlias)
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | db.sqlite3
3 | *.swp
4 | dump.rdb
5 | node_modules
6 | coverage
7 | .env
8 | webpack-stats.json
9 | yarn-error.log
10 | test-results
11 | .coverage
12 | .hypothesis
13 | .pytest_cache
14 | htmlcov
15 | prof
16 | stats.json
17 |
--------------------------------------------------------------------------------
/onboarding/management/commands/new_neighbors.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 | from onboarding import jobs
3 |
4 | class Command(BaseCommand):
5 |
6 | def handle(self, *args, **options):
7 | jobs.processAllTargets()
8 |
--------------------------------------------------------------------------------
/assets/js/components/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { App } from './App'
3 | import { mount } from 'enzyme'
4 |
5 | it('should safely render a default state', () => {
6 | const wrapper = mount()
7 | expect(wrapper.find('.the-app')).toHaveLength(1)
8 | })
9 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nikolaik/python-nodejs:python2.7-nodejs8
2 | ENV PYTHONUNBUFFERED 1
3 | RUN pip install pipenv
4 | RUN mkdir /code
5 | WORKDIR /code
6 | COPY package.json ./
7 | COPY Pipfile Pipfile.lock ./
8 | RUN pipenv -v --two --where sync
9 | RUN npm install
10 | COPY . /code/
11 |
--------------------------------------------------------------------------------
/assets/js/components/MapIndex.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { MapIndex } from './MapIndex'
3 | import { shallow } from 'enzyme'
4 | import Immutable from 'immutable'
5 |
6 | it('should render default state', () => {
7 | shallow()
8 | })
9 |
--------------------------------------------------------------------------------
/assets/js/components/MapIndex.scss:
--------------------------------------------------------------------------------
1 | .membership-map {
2 | overflow: hidden;
3 | width: 100%;
4 | height: 100%;
5 | flex: auto;
6 | display: flex;
7 | flex-flow: column;
8 |
9 | .leaflet-container {
10 | position: relative;
11 | width: inherit;
12 | flex: auto;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/assets/js/Django.js:
--------------------------------------------------------------------------------
1 | const getCookie = name => {
2 | const cookie = document.cookie
3 | .split(';')
4 | .find(cookie => cookie.trim().startsWith(name + '=')) || ''
5 | return decodeURIComponent(cookie.trim().substring(name.length + 1))
6 | }
7 |
8 | export var csrftoken = getCookie('csrftoken')
9 |
--------------------------------------------------------------------------------
/sync/templates/admin/sync/add.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/change_form.html" %}
2 | {% block content %}
3 |
Available backends:
4 |
5 | {% for backend in backends %}
6 | - {{backend}}
7 | {% endfor %}
8 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/assets/js/components/chrome/Logo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import EBFESymbol from 'svg-react-loader!../../../img/symbol.svg'
3 |
4 | const Logo = props => (
5 | (window.ORG_METADATA || {}).logo_url ?
:
6 | )
7 |
8 | export default Logo
9 |
--------------------------------------------------------------------------------
/assets/js/components/PeopleIndex.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import { PeopleIndex } from './PeopleIndex'
4 | import Immutable from 'immutable'
5 |
6 | it('should render default state', () => {
7 | shallow()
8 | })
9 |
--------------------------------------------------------------------------------
/crm/pipeline.py:
--------------------------------------------------------------------------------
1 | from models import Person
2 |
3 | def ensure_person_for_email(user, details, *args, **kwargs):
4 | person, _ = Person.objects.update_or_create(
5 | email=user.email,
6 | defaults=dict(
7 | name = details.get('name')
8 | )
9 | )
10 | return {'person': person}
11 |
--------------------------------------------------------------------------------
/assets/js/components/Gravatar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import gravatar from 'gravatar'
3 | import Avatar from '@material-ui/core/Avatar'
4 |
5 | export const Gravatar = props => (
6 |
9 | )
10 |
11 | export default Gravatar
12 |
--------------------------------------------------------------------------------
/assets/js/selectors/index.js:
--------------------------------------------------------------------------------
1 | export const Geocache = require('./geocache')
2 | export const Auth = require('./auth')
3 |
4 | export const getSaving = state => state.getIn(['model', 'saving'])
5 | export const getLoading = state => state.getIn(['model', 'loading'])
6 |
7 | export const getActivityCount = state => getSaving(state) + getLoading(state)
8 |
--------------------------------------------------------------------------------
/sync/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from . import models
3 |
4 | class ImportSourceForm(forms.ModelForm):
5 | backend = forms.CharField(disabled=True)
6 | class Meta:
7 | model = models.ImportSource
8 | fields = ['name', 'enabled', 'backend']
9 |
10 | class DefaultImportSourceConfigForm(forms.Form):
11 | pass
12 |
--------------------------------------------------------------------------------
/crm/templates/action.html:
--------------------------------------------------------------------------------
1 | {% extends 'layout.html' %}
2 | {% block title %}{{action.name}}{% endblock %}
3 | {% block subtitle %}{{action.date}}{% endblock %}
4 |
5 | {% block contents %}
6 | Forms
7 |
8 | {% for form in action.forms %}
9 |
10 | | {{form.title}} |
11 |
12 | {% endfor %}
13 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/assets/js/components/mapping/LocalMap.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import BaseMap from './BaseMap'
3 | import LocatorControl from './LocatorControl'
4 |
5 | const LocalMap = props => {
6 | return (
7 |
8 |
9 | {props.children}
10 |
11 | )
12 | }
13 |
14 | export default LocalMap
15 |
--------------------------------------------------------------------------------
/assets/js/selectors/geocache.js:
--------------------------------------------------------------------------------
1 | import { point } from '@turf/helpers'
2 |
3 | export const getCurrentLocation = state => state.getIn(['geocache', 'currentLocation']) ? point(state.getIn(['geocache', 'currentLocation'])) : null
4 | export const getAccuracy = state => state.getIn(['geocache', 'accuracy'])
5 |
6 | export const getLocationStatus = state => state.getIn(['geocache', 'status'])
7 |
--------------------------------------------------------------------------------
/geocodable/management/commands/resolve_all.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 |
3 | from geocodable import models
4 |
5 | class Command(BaseCommand):
6 | def handle(self, *args, **options):
7 | for alias in models.LocationAlias.objects.all():
8 | print alias.location.fullName
9 | alias.location = None
10 | alias.save()
11 |
--------------------------------------------------------------------------------
/assets/js/components/UserAvatar.js:
--------------------------------------------------------------------------------
1 | import Gravatar from './Gravatar'
2 | import { connect } from 'react-redux'
3 |
4 | import { getCurrentUser } from '../selectors/auth'
5 |
6 | const mapStateToProps = state => {
7 | return {
8 | email: getCurrentUser(state).email
9 | }
10 | }
11 |
12 | export const UserAvatar = connect(mapStateToProps)(Gravatar)
13 |
14 | export default UserAvatar
15 |
--------------------------------------------------------------------------------
/assets/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "EBF Organizer",
3 | "name": "East Bay Forward Organizer",
4 | "display": "standalone",
5 | "background_color": "#d7df23",
6 | "theme_color": "#d7df23",
7 | "icons": [
8 | {
9 | "src": "/static/img/logo-square.png",
10 | "type": "image/png",
11 | "sizes": "801x801"
12 | }
13 | ],
14 | "start_url": "/organize/"
15 | }
16 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = --forked
3 | DJANGO_SETTINGS_MODULE = organizer.settings
4 | python_files = tests.py test_*.py *_tests.py
5 | norecursedirs = assets node_modules .git
6 | markers =
7 | mock_redis: mark test to run with a mocked redis connection
8 | mock_geocoder: mark test to run with a mocked geocoder adaptor
9 | skip_auth: mark test to run without API authentication
10 |
--------------------------------------------------------------------------------
/docs/usage.rst:
--------------------------------------------------------------------------------
1 | Usage
2 | =====
3 |
4 | At its core, Organizer tries to be a useful CRM for grassroots organizations
5 | that hate using CRMs. No CRM is perfect, and no CRM will fit your organization
6 | exactly right. But Organizer does have one advantage: it is AGPLv3.
7 |
8 | .. toctree::
9 | :maxdepth: 3
10 | :caption: Features
11 |
12 | crm
13 | events
14 | import-export
15 | broadcasts
16 |
--------------------------------------------------------------------------------
/assets/js/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux-immutable'
2 | import model from './model'
3 | import geocache from './geocache'
4 | import selections from './select'
5 | import auth from './auth'
6 | import filters from './filters'
7 |
8 | const organizerApp = combineReducers({
9 | auth,
10 | model,
11 | selections,
12 | filters,
13 | geocache
14 | })
15 |
16 | export default organizerApp
17 |
--------------------------------------------------------------------------------
/assets/js/store/provider.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Provider } from 'react-redux'
3 | import { store } from './store'
4 |
5 | export const withProvider = Component => {
6 | return function wrapped(props) {
7 | return (
8 |
9 |
10 |
11 | )
12 | }
13 | }
14 |
15 | export default withProvider
16 |
--------------------------------------------------------------------------------
/crm/context.py:
--------------------------------------------------------------------------------
1 | from . import serializers
2 | from django.conf import settings
3 | import json
4 |
5 | def add_user_data(request):
6 | user_serializer = serializers.UserSerializer(request.user,
7 | context={'request': request})
8 | return {
9 | 'user_data': json.dumps(user_serializer.data)
10 | }
11 |
12 | def add_settings(request):
13 | return {
14 | 'settings': settings
15 | }
16 |
--------------------------------------------------------------------------------
/onboarding/templates/new-neighbors-notification.eml:
--------------------------------------------------------------------------------
1 | You have some new neighbors!
2 |
3 | {% for newbie in newbies %}* {{newbie.current_turf}} {{newbie.name}} <{{newbie.email}}>
4 | {% endfor %}
5 |
6 | Why not send them an e-mail and say hello?
7 |
8 | ----
9 | You are receiving this e-mail because EBFE Organizer is configured to notify
10 | this address about people nearby interested in organizing with East Bay for
11 | Everyone.
12 |
--------------------------------------------------------------------------------
/sync/templates/admin/sync/change_form.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/change_form.html" %}
2 | {% load i18n admin_urls %}
3 |
4 | {% block field_sets %}
5 |
10 |
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/crm/migrations/0016_merge_20190508_0013.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-05-08 00:13
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0015_auto_20190507_2326'),
12 | ('crm', '0013_convert_to_geocodable'),
13 | ]
14 |
15 | operations = [
16 | ]
17 |
--------------------------------------------------------------------------------
/assets/js/components/mapping/MarkerMap.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import BaseMap from './BaseMap'
3 | import { Marker } from 'react-leaflet'
4 |
5 | const MarkerMap = props => (
6 |
7 |
8 |
9 | )
10 |
11 | export default MarkerMap
12 |
--------------------------------------------------------------------------------
/geocodable/api_views.py:
--------------------------------------------------------------------------------
1 | from rest_framework.permissions import IsAuthenticated
2 | from . import models, serializers
3 | from organizer.viewsets import IntrospectiveViewSet
4 |
5 | class LocationViewSet(IntrospectiveViewSet):
6 | permission_classes = (IsAuthenticated,)
7 | queryset = models.Location.objects.all()
8 | serializer_class = serializers.LocationSerializer
9 |
10 | views = {
11 | 'locations': LocationViewSet
12 | }
13 |
--------------------------------------------------------------------------------
/onboarding/api_views.py:
--------------------------------------------------------------------------------
1 | from rest_framework import viewsets
2 | from rest_framework.permissions import AllowAny
3 | from crm.api_views import IntrospectiveViewSet
4 | from . import models, serializers
5 |
6 | class SignupViewSet(IntrospectiveViewSet):
7 | permission_classes = (AllowAny,)
8 | queryset = models.Signup.objects.all()
9 | serializer_class = serializers.SignupSerializer
10 |
11 | views = {
12 | 'signups': SignupViewSet
13 | }
14 |
--------------------------------------------------------------------------------
/docs/import-export.rst:
--------------------------------------------------------------------------------
1 | .. _import-export:
2 |
3 | Import and Export
4 | =================
5 |
6 | Organizer's :ref:`administration-interface` includes buttons for importing and
7 | exporting lists of data in CSV and other formats. Use the filters to narrow your
8 | list, then export it.
9 |
10 | Other services are processed by running the import or export sub-commands of
11 | ./manage.py. See :ref:`integrations` for a list of import and export plugins.
12 |
--------------------------------------------------------------------------------
/assets/js/reducers/filters.js:
--------------------------------------------------------------------------------
1 | import * as Filter from '../store/filter'
2 | import Immutable from 'immutable'
3 |
4 | export default function(state = Immutable.Map(), action = {}) {
5 | switch (action.type) {
6 | case Filter.SET_FILTER:
7 | return state.setIn(['filters', action.key], action.filter)
8 | default:
9 | return Immutable.Map({
10 | filters: Immutable.Map(),
11 | }).merge(state)
12 | }
13 | }
14 |
15 |
16 |
--------------------------------------------------------------------------------
/assets/js/components/chrome/LoadingSpinner.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import CircularProgress from '@material-ui/core/CircularProgress'
3 | import Grid from '@material-ui/core/Grid'
4 |
5 | export const LoadingSpinner = _props => (
6 |
7 |
8 |
9 | )
10 |
11 | export default LoadingSpinner
12 |
--------------------------------------------------------------------------------
/events/api_views.py:
--------------------------------------------------------------------------------
1 | from rest_framework import viewsets
2 | from rest_framework.permissions import IsAuthenticatedOrReadOnly
3 | from crm.api_views import IntrospectiveViewSet
4 | from . import models, serializers
5 |
6 | class EventViewSet(IntrospectiveViewSet):
7 | permission_classes = (IsAuthenticatedOrReadOnly,)
8 | queryset = models.Event.objects.all()
9 | serializer_class = serializers.EventSerializer
10 |
11 | views = {
12 | 'events': EventViewSet
13 | }
14 |
--------------------------------------------------------------------------------
/assets/js/Breakpoint.js:
--------------------------------------------------------------------------------
1 | export default class Breakpoint {
2 | constructor(breakpoints) {
3 | this.breakpoints = breakpoints
4 | }
5 |
6 | get(value) {
7 | return this.breakpoints.filter(breakpoint =>
8 | value <= breakpoint[0] || breakpoint[0] == undefined
9 | )[0]
10 | }
11 |
12 | getPoint(value) {
13 | return this.get(value)[0]
14 | }
15 |
16 | getValue(value) {
17 | return this.get(value)[1]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/assets/js/actions/index.js:
--------------------------------------------------------------------------------
1 | import * as _geocache from './geocache'
2 | export const Geocache = _geocache
3 |
4 | export const RECEIVE_USER = 'RECEIVE_USER'
5 |
6 | export const logout = () => {
7 | return dispatch => {
8 | return fetch('/api/users/logout/').then(() =>
9 | Promise.resolve(dispatch(receiveUser({})))
10 | )
11 | }
12 | }
13 |
14 | export const receiveUser = (u) => {
15 | return {
16 | type: RECEIVE_USER,
17 | user: u
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/assets/js/selectors/people.js:
--------------------------------------------------------------------------------
1 | import { getCurrentUser } from './auth'
2 | import { createSelector } from 'reselect'
3 | import { Model } from '../store'
4 |
5 | const People = new Model('people')
6 |
7 | export const getPeople = state => (
8 | People.immutableSelect(state)
9 | )
10 |
11 | export const getCurrentPerson = createSelector(
12 | getCurrentUser,
13 | getPeople,
14 | (currentUser, allPeople) => (
15 | currentUser ? allPeople.get(currentUser.email) : undefined
16 | )
17 | )
18 |
--------------------------------------------------------------------------------
/crm/migrations/0017_remove_person_address.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-05-08 00:23
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0016_merge_20190508_0013'),
12 | ]
13 |
14 | operations = [
15 | migrations.RemoveField(
16 | model_name='person',
17 | name='address',
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/assets/js/components/people-browser/BooleanSelect.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import MenuItem from '@material-ui/core/MenuItem'
3 |
4 | import MaterialFormSelect from '../MaterialFormSelect'
5 |
6 | export const BooleanSelect = props => {
7 | return (
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
15 | export default BooleanSelect
16 |
--------------------------------------------------------------------------------
/docs/events.rst:
--------------------------------------------------------------------------------
1 | .. _events:
2 |
3 | Events
4 | ======
5 |
6 | Organizer's user facing UI allows for members to check-in to events, just like
7 | paper and pen sign-in sheets but without the need to type them all up later on.
8 |
9 | Events must have an address and a time in order that they show up for members.
10 | Members can check-in to an event if they've enabled geolocation on their device
11 | and are within 1/4mi of the event's location.
12 |
13 | Event attendance can be later viewed through the :ref:`CRM`
14 |
--------------------------------------------------------------------------------
/events/migrations/0006_remove_event_address.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-05-10 23:54
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('events', '0005_auto_20190510_2349'),
12 | ]
13 |
14 | operations = [
15 | migrations.RemoveField(
16 | model_name='event',
17 | name='address',
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/events/tests.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.test import TestCase
5 |
6 | from . import importing
7 | from mock import patch
8 |
9 | def testGoogleEventImports():
10 | with patch('events.importing.GoogleCalendarImporter.get_google_events') as get_google_events:
11 | get_google_events.return_value = iter([])
12 | importer = importing.GoogleCalendarImporter({})
13 | importer.init()
14 | for page in importer:
15 | pass
16 |
--------------------------------------------------------------------------------
/sync/migrations/0004_auto_20190731_1553.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-07-31 15:53
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('sync', '0003_synctarget_lastrun'),
12 | ]
13 |
14 | operations = [
15 | migrations.RenameModel(
16 | old_name='SyncTarget',
17 | new_name='ImportSource',
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/assets/js/store/index.js:
--------------------------------------------------------------------------------
1 | import { store } from './store'
2 | export { store } from './store'
3 | export { default as Model, withModelData } from './model'
4 | export { default as Selectable } from './select'
5 | export { default as Filterable } from './filter'
6 | export { default as history } from './history'
7 | export { default as withProvider } from './provider'
8 |
9 | if (module.hot) {
10 | module.hot.accept('../reducers', () => {
11 | store.replaceReducer(require('../reducers').default)
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/crm/management/commands/make_superuser.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from django.core.management.base import BaseCommand, CommandError
3 |
4 | class Command(BaseCommand):
5 | def add_arguments(self, parser):
6 | parser.add_argument('email')
7 |
8 | def handle(self, *args, **options):
9 | user = User.objects.get(email=options['email'])
10 | user.is_superuser = True
11 | user.is_staff = True
12 | user.save()
13 | print "Updated %s to superuser"%(user)
14 |
--------------------------------------------------------------------------------
/onboarding/migrations/0008_remove_signup_deleted.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2018-10-16 21:36
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('onboarding', '0007_auto_20181016_0515'),
12 | ]
13 |
14 | operations = [
15 | migrations.RemoveField(
16 | model_name='signup',
17 | name='deleted',
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/onboarding/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers, relations
2 | from crm.serializers import AddressSerializer, PersonSerializer
3 | from . import models
4 | from events.models import Event
5 |
6 | class SignupSerializer(serializers.HyperlinkedModelSerializer):
7 | event = serializers.SlugRelatedField(
8 | queryset=Event.objects.all(), slug_field='id', required=False)
9 | class Meta:
10 | model = models.Signup
11 | fields = ('id', 'email', 'url', 'approved', 'created', 'event')
12 |
--------------------------------------------------------------------------------
/crm/migrations/0013_remove_turfmembership_is_captain.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-05-07 22:49
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0012_person_is_captain'),
12 | ]
13 |
14 | operations = [
15 | migrations.RemoveField(
16 | model_name='turfmembership',
17 | name='is_captain',
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/assets/js/components/chrome/BusyIndicator.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import CircularProgress from '@material-ui/core/CircularProgress'
4 | import { getActivityCount } from '../../selectors'
5 |
6 | const mapStateToProps = state => {
7 | return {
8 | queueSize: getActivityCount(state)
9 | }
10 | }
11 |
12 | export const BusyIndicator = props => (
13 | (props.queueSize > 0) ? : null
14 | )
15 |
16 | export default connect(mapStateToProps)(BusyIndicator)
17 |
--------------------------------------------------------------------------------
/donations/migrations/0002_auto_20181021_0226.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2018-10-21 02:26
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('donations', '0001_initial'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='donation',
17 | name='value',
18 | field=models.IntegerField(),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/donations/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models
5 | from crm.models import Person
6 |
7 | class Donation(models.Model):
8 | person = models.ForeignKey(Person, related_name='donations')
9 | timestamp = models.DateTimeField()
10 | value = models.IntegerField()
11 | transaction_id = models.CharField(max_length=200, blank=True)
12 | recurring = models.BooleanField()
13 |
14 | def __unicode__(self):
15 | return "${0} from {1}".format(self.value / 100.0, self.person)
16 |
--------------------------------------------------------------------------------
/organizer/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for organizer project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 | from raven.contrib.django.raven_compat.middleware.wsgi import Sentry
14 |
15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "organizer.settings")
16 |
17 | application = get_wsgi_application()
18 | application = Sentry(application)
19 |
--------------------------------------------------------------------------------
/sync/migrations/0003_synctarget_lastrun.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-07-31 15:31
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('sync', '0002_auto_20190730_1850'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='synctarget',
17 | name='lastRun',
18 | field=models.DateTimeField(blank=True, null=True),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/crm/migrations/0011_person_phone.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.1 on 2018-09-11 23:59
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0010_auto_20180820_0223'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='person',
17 | name='phone',
18 | field=models.CharField(blank=True, default=None, max_length=200, null=True),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/crm/migrations/0015_auto_20190507_2326.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-05-07 23:26
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0014_auto_20190507_2318'),
12 | ]
13 |
14 | operations = [
15 | migrations.RemoveField(
16 | model_name='person',
17 | name='state',
18 | ),
19 | migrations.DeleteModel(
20 | name='PersonState',
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/crm/migrations/0010_auto_20180820_0223.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.1 on 2018-08-20 02:23
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0009_auto_20180807_0304'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='person',
17 | name='name',
18 | field=models.CharField(blank=True, default='', max_length=200, null=True),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/onboarding/migrations/0006_auto_20180814_1829.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.1 on 2018-08-14 18:29
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('onboarding', '0005_auto_20180810_0756'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='newneighbornotificationtarget',
17 | name='turfs',
18 | field=models.ManyToManyField(to='crm.Turf'),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/onboarding/migrations/0011_auto_20181023_0619.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2018-10-23 06:19
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('onboarding', '0010_auto_20181023_0617'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='onboardingcomponent',
17 | name='configuration',
18 | field=models.TextField(blank=True, default=''),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/assets/js/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import * as Actions from '../actions'
3 |
4 | export default function(state = Immutable.Map(), action={}) {
5 | switch (action.type) {
6 | case Actions.RECEIVE_USER:
7 | return state.merge({
8 | user: action.user
9 | })
10 | default: {
11 | const hasInline = (typeof window.CURRENT_USER != 'undefined')
12 | const defaultUser = hasInline ? window.CURRENT_USER : {}
13 | return Immutable.Map({
14 | user: defaultUser,
15 | }).merge(state)
16 | }
17 | }
18 | }
19 |
20 |
21 |
--------------------------------------------------------------------------------
/onboarding/migrations/0002_newneighbornotificationtarget_last_notified.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.1 on 2018-08-02 03:20
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('onboarding', '0001_initial'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='newneighbornotificationtarget',
17 | name='last_notified',
18 | field=models.DateField(null=True),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/crm/migrations/0018_auto_20190510_2349.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-05-10 23:49
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0017_remove_person_address'),
12 | ]
13 |
14 | operations = [
15 | migrations.RemoveField(
16 | model_name='person',
17 | name='lat',
18 | ),
19 | migrations.RemoveField(
20 | model_name='person',
21 | name='lng',
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/crm/views.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.shortcuts import render
5 | from . import models, serializers
6 | from django.views.decorators.clickjacking import xframe_options_exempt
7 | from django.http import HttpResponse
8 | import json
9 |
10 | def index(request, *args, **kwargs):
11 | return render(request, 'webpack-index.html')
12 |
13 | def service_worker(request, *args, **kwargs):
14 | return render(request, 'service-worker.js', content_type='text/javascript')
15 |
16 | def robots(request, *args, **kwargs):
17 | return render(request, 'robots.txt')
18 |
--------------------------------------------------------------------------------
/events/migrations/0005_auto_20190510_2349.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-05-10 23:49
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('events', '0004_event_location'),
12 | ]
13 |
14 | operations = [
15 | migrations.RemoveField(
16 | model_name='event',
17 | name='lat',
18 | ),
19 | migrations.RemoveField(
20 | model_name='event',
21 | name='lng',
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/static/app.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #888;
3 | font-color: #fff;
4 | }
5 |
6 | .logo a {
7 | margin: 2em 2em 2em 0;
8 | display: block;
9 | color: transparent;
10 | height: 200px;
11 | background: url(logo.png) center center no-repeat;
12 | background-size: contain;
13 | }
14 |
15 | header {
16 | background: url(bg.png) repeat;
17 | }
18 |
19 | .mastiff {
20 | padding: 3em;
21 | color: #000;
22 | }
23 |
24 | .mastiff h1, .mastiff h2 {
25 | background-color: #fff;
26 | padding: 0.25em;
27 | }
28 |
29 | .canvas {
30 | background-color: #fff;
31 | }
32 |
33 | footer {
34 | font-size: small;
35 | }
36 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | db:
5 | image: postgres
6 | ports:
7 | - "5432:5432"
8 | redis:
9 | image: redis
10 | app:
11 | build: .
12 | environment:
13 | DATABASE_URL: "postgres://postgres@db:5432/postgres"
14 | REDISTOGO_URL: "redis://redis:6379/0"
15 | ALLOWED_HOSTS: "*"
16 | DEBUG: 1
17 | SECRET_KEY: 'fyq9-o@ky!j*xx0y2mpyi92&wpbbjgo%z1@vrmzk18hpje+%x'
18 | command: pipenv run npm run start:docker
19 | volumes:
20 | - .:/code
21 | ports:
22 | - "8000:8000"
23 | - "8080:8080"
24 | depends_on:
25 | - db
26 | - redis
27 |
--------------------------------------------------------------------------------
/geocodable/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from . import models
3 |
4 | class LocationSerializer(serializers.HyperlinkedModelSerializer):
5 | type = serializers.SerializerMethodField()
6 |
7 | def get_type(self, obj):
8 | if obj.type is None:
9 | return None
10 | return obj.type.value
11 |
12 | class Meta:
13 | model = models.Location
14 | fields = ('name', 'parent', 'id', 'fullName', 'type', 'lat', 'lng')
15 |
16 | class GeoSerializer(serializers.ModelSerializer):
17 | class Meta:
18 | model = models.LocationAlias
19 | fields = ('lat', 'lng', 'fullName')
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/assets/js/reducers/geocache.js:
--------------------------------------------------------------------------------
1 | import * as Actions from '../actions'
2 | import Immutable from 'immutable'
3 |
4 | export default function(state = Immutable.Map(), action = {}) {
5 | switch (action.type) {
6 | case Actions.Geocache.UPDATE_CURRENT_LOCATION:
7 | return state.set('accuracy', action.accuracy).set('currentLocation', action.geo)
8 | case Actions.Geocache.SET_LOCATION_STATUS:
9 | return state.set('status', action.status)
10 | default:
11 | return Immutable.Map({
12 | currentLocation: undefined,
13 | status: undefined,
14 | accuracy: undefined
15 | }).merge(state)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/assets/js/components/chrome/BusyIndicator.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Raven from 'raven-js'
3 | import { shallow } from 'enzyme'
4 | import CircularProgress from '@material-ui/core/CircularProgress'
5 |
6 | import { BusyIndicator } from './BusyIndicator'
7 |
8 | it('should render nothing when there is nothing happening', () => {
9 | const wrapper = shallow()
10 | expect(wrapper.find(CircularProgress)).toHaveLength(0)
11 | })
12 |
13 | it('should render something when there is nothing happening', () => {
14 | const wrapper = shallow()
15 | expect(wrapper.find(CircularProgress)).toHaveLength(1)
16 | })
17 |
--------------------------------------------------------------------------------
/assets/js/components/people-browser/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 |
4 | import { Search } from './Search'
5 | import { BooleanFilter } from './BooleanFilter'
6 | import { BooleanSelect } from './BooleanSelect'
7 |
8 | describe('Search', () => {
9 | it('should render defaults safely', () => {
10 | shallow()
11 | })
12 | })
13 |
14 | describe('BooleanFilter', () => {
15 | it('should render defaults safely', () => {
16 | shallow()
17 | })
18 | })
19 |
20 | describe('BooleanSelect', () => {
21 | it('should render defaults safely', () => {
22 | shallow()
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/donations/admin.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.contrib import admin
5 | from import_export.admin import ImportExportModelAdmin
6 | from organizer.admin import admin_site, OrganizerModelAdmin
7 |
8 | from . import models, importing
9 |
10 | class DonationAdmin(ImportExportModelAdmin, OrganizerModelAdmin):
11 | resource_class = importing.DonationResource
12 |
13 | list_display = (
14 | 'value', 'person', 'timestamp', 'recurring'
15 | )
16 |
17 | list_filter = (
18 | 'person', 'recurring'
19 | )
20 |
21 | admin.site.register(models.Donation, DonationAdmin)
22 | admin_site.register(models.Donation, DonationAdmin)
23 |
--------------------------------------------------------------------------------
/onboarding/migrations/0013_auto_20190402_2358.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-04-02 23:58
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('onboarding', '0012_auto_20181107_2352'),
13 | ]
14 |
15 | operations = [
16 | migrations.AlterField(
17 | model_name='signup',
18 | name='event',
19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='signups', to='events.Event'),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/assets/scss/app.scss:
--------------------------------------------------------------------------------
1 | .the-app {
2 | background-color: #fff;
3 | display: flex;
4 | flex-flow: column;
5 | max-height: 100%;
6 | height: inherit;
7 | }
8 |
9 | .viewport {
10 | display: flex;
11 | flex: auto;
12 | overflow-y: auto;
13 | }
14 |
15 | .viewport .scroll {
16 | display: block;
17 | flex: auto;
18 | width: 100%;
19 | }
20 |
21 | .bottom-nav {
22 | align-self: flex-end;
23 | flex: none;
24 | width: 100%;
25 | }
26 |
27 | body {
28 | display: flex;
29 | background-color: #fff;
30 | font-family: "Roboto";
31 | height: inherit;
32 | flex-flow: column;
33 | margin: 0;
34 | }
35 |
36 | html {
37 | height: 100%;
38 | }
39 |
40 | #container {
41 | height: inherit;
42 | }
43 |
--------------------------------------------------------------------------------
/onboarding/migrations/0010_auto_20181023_0617.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2018-10-23 06:17
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('onboarding', '0009_auto_20181021_2234'),
13 | ]
14 |
15 | operations = [
16 | migrations.AlterField(
17 | model_name='onboardingstatus',
18 | name='component',
19 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='statuses', to='onboarding.OnboardingComponent'),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/assets/js/actions/index.test.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_USER, logout } from './'
2 | import fetchMock from 'fetch-mock'
3 | import configureMockStore from 'redux-mock-store'
4 | import thunk from 'redux-thunk'
5 |
6 | const mockStore = configureMockStore([thunk])
7 |
8 | beforeEach(() => {
9 | fetchMock.restore()
10 | })
11 |
12 | it('should fetch the logout endpoint when the logout action is dispatched', () => {
13 | const store = mockStore()
14 | fetchMock.mock('/api/users/logout/', {})
15 | return store.dispatch(logout())
16 | .then(() => {
17 | expect(store.getActions()).toHaveLength(1)
18 | expect(store.getActions()[0]).toEqual({type: RECEIVE_USER, user: {}})
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/crm/migrations/0006_auto_20180802_0712.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.1 on 2018-08-02 07:12
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0005_auto_20180802_0319'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='person',
17 | name='lat',
18 | field=models.FloatField(null=True),
19 | ),
20 | migrations.AlterField(
21 | model_name='person',
22 | name='lng',
23 | field=models.FloatField(null=True),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/assets/js/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'enzyme'
3 | import App from './components/App'
4 | import { store } from './store'
5 | import { Provider } from 'react-redux'
6 |
7 | it('should render defaults safely', () => {
8 | // FIXME: "unknown tag" invariant??
9 | render()
10 | })
11 |
12 | it('should import index.js and boot up to an App', () => {
13 | jest.mock('raven-js')
14 | jest.mock('@material-ui/styles')
15 | require('@material-ui/styles').install = jest.fn()
16 | document.body.innerHTML = ``
17 |
18 | require('./index')
19 | expect(require('@material-ui/styles').install).toHaveBeenCalled()
20 | })
21 |
--------------------------------------------------------------------------------
/assets/js/store/store.js:
--------------------------------------------------------------------------------
1 | import organizerApp from '../reducers'
2 |
3 | import { compose, createStore, applyMiddleware } from 'redux'
4 | import { getCurrentUser } from '../selectors/auth'
5 | import thunkMiddleware from 'redux-thunk'
6 | import createRavenMiddleware from 'raven-for-redux'
7 | import Raven from 'raven-js'
8 | import { connectRouter, routerMiddleware } from 'connected-react-router/immutable'
9 |
10 | const composer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
11 |
12 | export const store = createStore(
13 | connectRouter(history)(organizerApp),
14 | composer(applyMiddleware(createRavenMiddleware(Raven, {getUserContext: getCurrentUser} ), thunkMiddleware, routerMiddleware(history)))
15 | )
16 | export default store
17 |
--------------------------------------------------------------------------------
/sync/migrations/0002_auto_20190730_1850.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-07-30 18:50
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('sync', '0001_initial'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='synctarget',
17 | name='enabled',
18 | field=models.BooleanField(default=False),
19 | ),
20 | migrations.AlterField(
21 | model_name='synctarget',
22 | name='configuration',
23 | field=models.TextField(blank=True, null=True),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/crm/migrations/0012_person_location.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-04-22 20:54
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('crm', '0011_person_phone'),
13 | ('geocodable', '0001_initial'),
14 | ]
15 |
16 |
17 | operations = [
18 | migrations.AddField(
19 | model_name='person',
20 | name='location',
21 | field=models.ForeignKey(blank=True, null=True,
22 | on_delete=django.db.models.deletion.CASCADE,
23 | to='geocodable.LocationAlias'),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/crm/migrations/0009_auto_20180807_0304.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.1 on 2018-08-07 03:04
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0008_auto_20180806_0008'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='person',
17 | name='email',
18 | field=models.EmailField(db_index=True, max_length=200, unique=True),
19 | ),
20 | migrations.AlterField(
21 | model_name='personstate',
22 | name='name',
23 | field=models.CharField(max_length=200, unique=True),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/onboarding/migrations/0014_auto_20190507_2318.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-05-07 23:18
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('onboarding', '0013_auto_20190402_2358'),
12 | ]
13 |
14 | operations = [
15 | migrations.RemoveField(
16 | model_name='newneighbornotificationtarget',
17 | name='states',
18 | ),
19 | migrations.RemoveField(
20 | model_name='newneighbornotificationtarget',
21 | name='turfs',
22 | ),
23 | migrations.DeleteModel(
24 | name='NewNeighborNotificationTarget',
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/assets/js/components/chrome/LoginButtons.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import LoginButtons from './LoginButtons'
3 | import { shallow } from 'enzyme'
4 | import Button from '@material-ui/core/Button'
5 | import fc from 'fast-check'
6 |
7 | it('should render an empty login splash', () => {
8 | const wrapper = shallow()
9 | expect(wrapper.find(Button)).toHaveLength(0)
10 | })
11 |
12 | it('should render a list of login URLs', () => {
13 | fc.assert(fc.property(fc.array(fc.tuple(fc.string(), fc.string())), urls => {
14 | global.LOGIN_URLS = urls
15 | global.ORG_METADATA = {name: 'Name', shortname: 'Shortname'}
16 | const wrapper = shallow()
17 | expect(wrapper.find(Button)).toHaveLength(urls.length)
18 | }))
19 | })
20 |
--------------------------------------------------------------------------------
/assets/js/store/filter.js:
--------------------------------------------------------------------------------
1 | export const SET_FILTER = 'SET_FILTER'
2 |
3 | export const setFilter = (key, filter) => {
4 | return {
5 | type: SET_FILTER,
6 | key: key,
7 | filter: filter
8 | }
9 | }
10 |
11 | const isEqual = (a, b) => a == b
12 |
13 | export default class Filterable {
14 | constructor(key, matcher = isEqual) {
15 | this.key = key
16 | this.matcher = matcher
17 | }
18 |
19 | filtered(state, values) {
20 | const filterConfig = state.getIn(['filters', 'filters', this.key])
21 | return values.filter(value => this.matcher(value, filterConfig))
22 | }
23 |
24 | bindActionCreators(dispatch) {
25 | return {
26 | set: f => dispatch(setFilter(this.key, f))
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/sync/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-07-30 17:37
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='SyncTarget',
18 | fields=[
19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20 | ('name', models.CharField(max_length=255)),
21 | ('backend', models.CharField(max_length=255)),
22 | ('configuration', models.TextField()),
23 | ],
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/onboarding/migrations/0004_auto_20180810_0755.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.1 on 2018-08-10 07:55
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0009_auto_20180807_0304'),
12 | ('onboarding', '0003_newneighbornotificationtarget_state'),
13 | ]
14 |
15 | operations = [
16 | migrations.RemoveField(
17 | model_name='newneighbornotificationtarget',
18 | name='state',
19 | ),
20 | migrations.AddField(
21 | model_name='newneighbornotificationtarget',
22 | name='states',
23 | field=models.ManyToManyField(to='crm.PersonState'),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/onboarding/migrations/0003_newneighbornotificationtarget_state.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.1 on 2018-08-05 23:35
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('crm', '0007_auto_20180805_2335'),
13 | ('onboarding', '0002_newneighbornotificationtarget_last_notified'),
14 | ]
15 |
16 | operations = [
17 | migrations.AddField(
18 | model_name='newneighbornotificationtarget',
19 | name='state',
20 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='crm.PersonState'),
21 | preserve_default=False,
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/onboarding/migrations/0005_auto_20180810_0756.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.1 on 2018-08-10 07:56
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('crm', '0009_auto_20180807_0304'),
12 | ('onboarding', '0004_auto_20180810_0755'),
13 | ]
14 |
15 | operations = [
16 | migrations.RemoveField(
17 | model_name='newneighbornotificationtarget',
18 | name='turf',
19 | ),
20 | migrations.AddField(
21 | model_name='newneighbornotificationtarget',
22 | name='turfs',
23 | field=models.ManyToManyField(related_name='notification_targets', to='crm.Turf'),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/assets/js/components/MaterialFormSwitch.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { mount } from 'enzyme'
3 | import { Form } from 'informed'
4 | import Switch from '@material-ui/core/Switch'
5 | import MenuItem from '@material-ui/core/MenuItem'
6 |
7 | import MaterialFormSwitch from './MaterialFormSwitch'
8 |
9 | it('should render defaults safely', () => {
10 | const onValueChange = jest.fn()
11 | const root = mount(
12 |
15 | )
16 | root.find(Switch).prop('onChange')({target: {checked: true}})
17 | expect(onValueChange).toHaveBeenCalledWith({select: true})
18 | root.find(Switch).prop('onChange')({target: {checked: false}})
19 | expect(onValueChange).toHaveBeenCalledWith({select: false})
20 | })
21 |
22 |
--------------------------------------------------------------------------------
/assets/js/index.js:
--------------------------------------------------------------------------------
1 | import './style-bootstrap'
2 |
3 | import React from 'react'
4 | import ReactDOM from 'react-dom'
5 | import '../scss/app.scss'
6 |
7 | import App from './components/App'
8 |
9 | import Raven from 'raven-js'
10 |
11 | Raven.config(window.SENTRY_PUBLIC_DSN).install()
12 | window.onunhandledrejection = function(e) {Raven.captureException(e.reason)}
13 |
14 | if ('serviceWorker' in navigator) {
15 | window.addEventListener('load', () => {
16 | navigator.serviceWorker.register('/service-worker.js').then(registration => {
17 | console.log('SW registered: ' + registration)
18 | }).catch(registrationError => {
19 | console.log('SW registration failed: ' + registrationError)
20 | })
21 | })
22 | }
23 |
24 | ReactDOM.render(
25 | ,
26 | document.getElementById('container')
27 | )
28 |
--------------------------------------------------------------------------------
/assets/js/components/MaterialFormSwitch.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { asField } from 'informed'
3 | import Switch from '@material-ui/core/Switch'
4 |
5 | const MaterialFormSwitch = asField(({fieldState, fieldApi, ...props}) => {
6 | const {
7 | value
8 | } = fieldState
9 | const {
10 | setValue,
11 | } = fieldApi
12 | const {
13 | onChange,
14 | forwardedRef,
15 | ...rest
16 | } = props
17 | return (
18 | {
23 | setValue(e.target.checked)
24 | if (onChange) {
25 | onChange(e)
26 | }
27 | }}
28 | />
29 | )
30 | })
31 |
32 | export default MaterialFormSwitch
33 |
--------------------------------------------------------------------------------
/onboarding/management/commands/onboard.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 | from onboarding.models import runOnboarding
3 | from crm.models import Person
4 | import logging
5 |
6 | log = logging.getLogger(__name__)
7 |
8 | class Command(BaseCommand):
9 | def add_arguments(self, parser):
10 | parser.add_argument('email', nargs='*', default=[])
11 | parser.add_argument('--dry-run', default=False, action='store_true')
12 |
13 | def handle(*args, **options):
14 | if len(options['email']) == 0:
15 | people = Person.objects.all()
16 | else:
17 | people = Person.objects.filter(email__in=options['email'])
18 |
19 | for person in people:
20 | if not options['dry_run']:
21 | runOnboarding.delay(person)
22 | log.info("Queued onboarding check for %s", person)
23 |
--------------------------------------------------------------------------------
/assets/js/selectors/events.test.js:
--------------------------------------------------------------------------------
1 | import { cookEventWithLocation } from './events'
2 | import moment from 'moment'
3 | import { point } from '@turf/helpers'
4 |
5 | const now = moment()
6 |
7 | it('should add required properties to the event', () => {
8 | const eventSkeleton = {
9 | geo: point([0, 0]),
10 | timestamp: now,
11 | end_timestamp: now,
12 | }
13 | const evt = cookEventWithLocation(point([0, 0]), 0, eventSkeleton, moment(), [])
14 | expect(evt).toMatchObject({
15 | distance: expect.any(Number),
16 | relevance: expect.any(Number),
17 | walktime: expect.any(Number),
18 | checkIn: {
19 | isNearby: expect.any(Boolean),
20 | isInPast: expect.any(Boolean),
21 | hasNotStarted: expect.any(Boolean),
22 | canCheckIn: expect.any(Boolean),
23 | }
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/assets/js/components/MaterialFormText.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { mount } from 'enzyme'
3 | import { Form } from 'informed'
4 | import TextField from '@material-ui/core/TextField'
5 | import MenuItem from '@material-ui/core/MenuItem'
6 |
7 | import MaterialFormText from './MaterialFormText'
8 |
9 | it('should render defaults safely', () => {
10 | const onValueChange = jest.fn()
11 | const onChange = jest.fn()
12 | const root = mount(
13 |
16 | )
17 | root.find(TextField).prop('onChange')({target: {value: "Text"}})
18 | expect(onValueChange).toHaveBeenCalledWith({select: "Text"})
19 | root.find(TextField).prop('onBlur')()
20 | expect(onChange.mock.calls[0][0]).toMatchObject({touched: {select: true}})
21 | })
22 |
23 |
--------------------------------------------------------------------------------
/sync/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models
5 | from django.contrib.auth.models import User
6 | import json
7 | from organizer.importing import get_importer_class
8 |
9 | #FIXME: This only supports importing data, no exporting or full sync is implemented yet
10 | class ImportSource(models.Model):
11 | name = models.CharField(max_length=255)
12 | backend = models.CharField(max_length=255)
13 | enabled = models.BooleanField(default=False)
14 | configuration = models.TextField(blank=True, null=True)
15 | lastRun = models.DateTimeField(blank=True, null=True)
16 |
17 | def make_importer(self):
18 | importCls = get_importer_class(self.backend)
19 | return importCls(json.loads(self.configuration))
20 |
21 | def __unicode__(self):
22 | return '%s: %s' % (self.name, self.backend)
23 |
--------------------------------------------------------------------------------
/assets/js/components/AppRoutes.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { AppRoutes } from './AppRoutes'
3 | import CheckinActivity from './activities/checkin'
4 | import { withProvider } from '../store'
5 | import { mount } from 'enzyme'
6 | import fetchMock from 'fetch-mock'
7 |
8 | const MountableRoutes = withProvider(AppRoutes)
9 |
10 | beforeEach(() => {
11 | fetchMock.restore()
12 | })
13 |
14 | it('should safely render a logged out state', () => {
15 | fetchMock.mock('begin:/api/events/', {})
16 | const wrapper = mount()
17 | expect(wrapper.find(CheckinActivity)).toHaveLength(1)
18 | })
19 |
20 | it('should safely render a logged in state', () => {
21 | fetchMock.mock('begin:/api/events/', {})
22 | const wrapper = mount()
23 | expect(wrapper.find(CheckinActivity)).toHaveLength(1)
24 | })
25 |
26 |
--------------------------------------------------------------------------------
/onboarding/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.1 on 2018-08-02 03:19
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | ('crm', '0005_auto_20180802_0319'),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='NewNeighborNotificationTarget',
20 | fields=[
21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22 | ('email', models.CharField(max_length=200)),
23 | ('turf', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_targets', to='crm.Turf')),
24 | ],
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/organizer/pipeline.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User, Group
2 |
3 | def sync_from_discourse_auth(user, details, **kwargs):
4 | for groupSlug in details.get('groups', []):
5 | groupSlug = 'discourse:' + groupSlug
6 | groupObj, _ = Group.objects.get_or_create(name=groupSlug)
7 | user.groups.add(groupObj)
8 |
9 | for group in user.groups.filter(name__startswith='discourse:'):
10 | _, groupName = group.name.split(':')
11 | if groupName not in details.get('groups', []):
12 | user.groups.remove(group)
13 |
14 | user.is_superuser = details.get('is_superuser', False)
15 | user.is_staff = details.get('is_staff', False)
16 | user.save()
17 |
18 | def sync_backend_group(user, backend, **kwargs):
19 | groupName = "social-auth:%s"%(backend.name)
20 | backend_group, _ = Group.objects.get_or_create(name=groupName)
21 | user.groups.add(backend_group)
22 |
--------------------------------------------------------------------------------
/assets/js/components/MaterialFormSelect.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { mount } from 'enzyme'
3 | import { Form } from 'informed'
4 | import Select from '@material-ui/core/Select'
5 | import MenuItem from '@material-ui/core/MenuItem'
6 |
7 | import MaterialFormSelect from './MaterialFormSelect'
8 |
9 | it('should render defaults safely', () => {
10 | const onValueChange = jest.fn()
11 | const onChange = jest.fn()
12 | const root = mount(
13 |
18 | )
19 | root.find(Select).prop('onChange')({target: 1})
20 | expect(onValueChange).toHaveBeenCalled()
21 | root.find(Select).prop('onBlur')()
22 | expect(onChange.mock.calls[0][0]).toMatchObject({touched: {select: true}})
23 | })
24 |
--------------------------------------------------------------------------------
/docs/broadcasts.rst:
--------------------------------------------------------------------------------
1 | .. _broadcasts:
2 |
3 | Broadcasts
4 | ==========
5 |
6 | Organizer has a limited neighborhood broadcast tool which sends e-mails to all
7 | members within a specific neighborhood or turf. It is available to neighborhood
8 | captains, which are defined through the Django administration UI (See
9 | :ref:`maintenance`).
10 |
11 |
12 | .. _new-neighbor-notifications:
13 |
14 | New Neighbor Notifications
15 | --------------------------
16 |
17 | In addition to neighborhood-wide message broadcasts, Organizer can be configured
18 | to e-mail your neighborhood captains (or really, any specific e-mail address)
19 | with a list of new people for each neighborhood in the :ref:`CRM` at regular
20 | intervals.
21 |
22 | It is intended to be ran on a regular schedule, such as once a week or every
23 | afternoon. To run it:
24 |
25 | ./manage.py new_neighbors
26 |
27 | The notifications may be configured through the :ref:`administration-interface`.
28 |
--------------------------------------------------------------------------------
/crm/templates/form.html:
--------------------------------------------------------------------------------
1 | {% extends 'layout.html' %}
2 | {% load static %}
3 |
4 | {% block extrahead %}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior:
12 | 1. Go to '...'
13 | 2. Click on '....'
14 | 3. Scroll down to '....'
15 | 4. See error
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Desktop (please complete the following information):**
24 | - OS: [e.g. iOS]
25 | - Browser [e.g. chrome, safari]
26 | - Version [e.g. 22]
27 |
28 | **Smartphone (please complete the following information):**
29 | - Device: [e.g. iPhone6]
30 | - OS: [e.g. iOS8.1]
31 | - Browser [e.g. stock browser, safari]
32 | - Version [e.g. 22]
33 |
34 | **Additional context**
35 | Add any other context about the problem here.
36 |
--------------------------------------------------------------------------------
/crm/migrations/0012_person_is_captain.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-05-07 22:42
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 | def copy_captain_flag(apps, schema_editor):
8 | Person = apps.get_model('crm', 'Person')
9 | TurfMembership = apps.get_model('crm', 'TurfMembership')
10 |
11 | for person in Person.objects.all():
12 | if TurfMembership.objects.filter(is_captain=True, person=person).exists():
13 | person.is_captain = True
14 | person.save()
15 |
16 | class Migration(migrations.Migration):
17 |
18 | dependencies = [
19 | ('crm', '0011_person_phone'),
20 | ]
21 |
22 | operations = [
23 | migrations.AddField(
24 | model_name='person',
25 | name='is_captain',
26 | field=models.BooleanField(default=False),
27 | ),
28 | migrations.RunPython(copy_captain_flag),
29 | ]
30 |
--------------------------------------------------------------------------------
/assets/js/components/events/NoEvents.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import faCalendar from '@fortawesome/fontawesome-free-solid/faCalendar'
4 | import { library as faLibrary } from '@fortawesome/fontawesome'
5 | import { withStyles } from '@material-ui/styles'
6 |
7 | faLibrary.add(faCalendar)
8 |
9 | export const NoEvents = props => (
10 | (props.show ? props.children : (
11 |
12 |
13 |
No events.
14 |
Go make some trouble.
15 |
16 | ))
17 | )
18 |
19 | NoEvents.defaultProps = {
20 | show: false
21 | }
22 |
23 | const styles = {
24 | root: {
25 | backgroundColor: '#ddd',
26 | color: '#aaa',
27 | textAlign: 'center',
28 | padding: '3rem'
29 | }
30 | }
31 |
32 | export default withStyles(styles)(NoEvents)
33 |
--------------------------------------------------------------------------------
/assets/js/components/mapping/BaseMap.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import L from 'leaflet'
3 |
4 | import markerIcon from 'leaflet/dist/images/marker-icon.png'
5 | import markerRetinaIcon from 'leaflet/dist/images/marker-icon-2x.png'
6 | import markerShadow from 'leaflet/dist/images/marker-shadow.png'
7 |
8 | import { TileLayer, Map } from 'react-leaflet'
9 |
10 | delete L.Icon.Default.prototype._getIconUrl
11 |
12 | import 'leaflet/dist/leaflet.css'
13 |
14 | L.Icon.Default.mergeOptions({
15 | iconRetinaUrl: markerRetinaIcon,
16 | iconUrl: markerIcon,
17 | shadowUrl: markerShadow
18 | })
19 |
20 | const BaseMap = props => (
21 |
27 | )
28 |
29 | BaseMap.defaultProps = {
30 | center: [0, 0],
31 | zoom: 17
32 | }
33 |
34 | export default BaseMap
35 |
--------------------------------------------------------------------------------
/crm/migrations/0014_auto_20190507_2318.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2019-05-07 23:18
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('onboarding', '0014_auto_20190507_2318'),
12 | ('crm', '0013_remove_turfmembership_is_captain'),
13 | ]
14 |
15 | operations = [
16 | migrations.RemoveField(
17 | model_name='turf',
18 | name='locality',
19 | ),
20 | migrations.RemoveField(
21 | model_name='turfmembership',
22 | name='person',
23 | ),
24 | migrations.RemoveField(
25 | model_name='turfmembership',
26 | name='turf',
27 | ),
28 | migrations.DeleteModel(
29 | name='Turf',
30 | ),
31 | migrations.DeleteModel(
32 | name='TurfMembership',
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | try:
7 | import dotenv
8 | dotenv.read_dotenv()
9 | except ImportError:
10 | pass
11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "organizer.settings")
12 | try:
13 | from django.core.management import execute_from_command_line
14 | except ImportError:
15 | # The above import may fail for some other reason. Ensure that the
16 | # issue is really that Django is missing to avoid masking other
17 | # exceptions on Python 2.
18 | try:
19 | import django
20 | except ImportError:
21 | raise ImportError(
22 | "Couldn't import Django. Are you sure it's installed and "
23 | "available on your PYTHONPATH environment variable? Did you "
24 | "forget to activate a virtual environment?"
25 | )
26 | raise
27 | execute_from_command_line(sys.argv)
28 |
--------------------------------------------------------------------------------
/assets/js/components/events/NoLocation.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import faCompass from '@fortawesome/fontawesome-free-solid/faCompass'
4 | import { library as faLibrary } from '@fortawesome/fontawesome'
5 | import { withStyles } from '@material-ui/styles'
6 | import Button from '@material-ui/core/Button'
7 |
8 | faLibrary.add(faCompass)
9 |
10 | export const NoLocation = props => (
11 |
12 |
13 |
{props.message}
14 | {props.onStartGeolocation ? (
) : null }
15 |
16 | )
17 |
18 | const styles = {
19 | root: {
20 | backgroundColor: '#ddd',
21 | color: '#aaa',
22 | textAlign: 'center',
23 | padding: '3rem'
24 | }
25 | }
26 |
27 | export default withStyles(styles)(NoLocation)
28 |
29 |
--------------------------------------------------------------------------------
/assets/js/components/activities/checkin/Skeleton.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { withStyles } from '@material-ui/styles'
3 |
4 | import Grid from '@material-ui/core/Grid'
5 |
6 | const carouselStyles = {
7 | card: {
8 | backgroundColor: '#eee',
9 | animationName: '$fade',
10 | animationDuration: '1s',
11 | animationIterationCount: 'infinite',
12 | animationDirection: 'alternate',
13 | animationTimingFunction: 'ease-out',
14 | height: '14rem',
15 | margin: '1rem'
16 | },
17 | '@keyframes fade': {
18 | from: {
19 | backgroundColor: '#fafafa'
20 | },
21 | to: {
22 | backgroundColor: '#ddd'
23 | }
24 | }
25 | }
26 |
27 | export const Skeleton = props => (
28 |
29 |
30 |
31 |
32 | )
33 |
34 | export default withStyles(carouselStyles)(Skeleton)
35 |
--------------------------------------------------------------------------------
/crm/migrations/0008_auto_20180806_0008.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.1 on 2018-08-06 00:08
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import taggit.managers
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('crm', '0007_auto_20180805_2335'),
13 | ]
14 |
15 | operations = [
16 | migrations.AlterField(
17 | model_name='person',
18 | name='lat',
19 | field=models.FloatField(blank=True, null=True),
20 | ),
21 | migrations.AlterField(
22 | model_name='person',
23 | name='lng',
24 | field=models.FloatField(blank=True, null=True),
25 | ),
26 | migrations.AlterField(
27 | model_name='person',
28 | name='tags',
29 | field=taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/donations/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.16 on 2018-10-21 01:36
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | ('crm', '0011_person_phone'),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='Donation',
20 | fields=[
21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22 | ('timestamp', models.DateTimeField()),
23 | ('value', models.FloatField()),
24 | ('transaction_id', models.CharField(blank=True, max_length=200)),
25 | ('recurring', models.BooleanField()),
26 | ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='donations', to='crm.Person')),
27 | ],
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/assets/js/reducers/model.js:
--------------------------------------------------------------------------------
1 | import * as Model from '../store/model'
2 | import Immutable from 'immutable'
3 |
4 | export default function(state = Immutable.Map(), action = {}) {
5 | if (!Immutable.isImmutable(state)) {
6 | state = Immutable.Map()
7 | }
8 | switch (action.type) {
9 | case Model.UPDATE_MODEL:
10 | return state.setIn(['models', action.name, action.id], action.data)
11 | case Model.SAVING_MODEL:
12 | return state.update('saving', i => i + 1)
13 | case Model.SAVED_MODEL:
14 | return state.update('saving', i => i - 1)
15 | case Model.REQUEST_MODELS:
16 | return state.update('loading', i => i + 1)
17 | case Model.RECEIVE_MODELS:
18 | return state.mergeIn(['models', action.name], Immutable.List(action.models).map(m => [m.id, m]))
19 | .update('loading', i => i - 1)
20 | default:
21 | return Immutable.Map({
22 | loading: 0,
23 | saving: false,
24 | modified: false,
25 | models: Immutable.Map(),
26 | }).merge(state)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/assets/js/components/chrome/OrganizerAppBar.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Raven from 'raven-js'
3 | import { shallow } from 'enzyme'
4 | import MenuItem from '@material-ui/core/MenuItem'
5 |
6 | import { AppMenu } from './OrganizerAppBar'
7 |
8 | jest.mock('raven-js')
9 |
10 | it('should trigger a report dialog when the user clicks the report button', () => {
11 | const wrapper = shallow()
12 | const menuItems = wrapper.find(MenuItem)
13 | menuItems.last().dive().simulate('click')
14 | expect(Raven.captureMessage).toHaveBeenCalledWith("Manual report")
15 | expect(Raven.showReportDialog).toHaveBeenCalled()
16 | })
17 |
18 | it('should log the user out when the user clicks the logout button', () => {
19 | const loggerOuter = jest.fn()
20 | const wrapper = shallow()
21 | const menuItems = wrapper.find(MenuItem)
22 | menuItems.first().dive().simulate('click')
23 | expect(loggerOuter).toHaveBeenCalled()
24 | })
25 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | var config = require('./webpack.config')
2 | var merge = require('webpack-merge')
3 | var MiniCssExtractPlugin = require('mini-css-extract-plugin')
4 | var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
5 | var UglifyJsPlugin = require('uglifyjs-webpack-plugin')
6 |
7 | module.exports = merge(config, {
8 | mode: 'production',
9 | plugins: [
10 | new MiniCssExtractPlugin()
11 | ],
12 | devtool: 'source-map',
13 | module: {
14 | rules: [
15 | {
16 | test: /\.s?css$/,
17 | use: [
18 | MiniCssExtractPlugin.loader,
19 | 'css-loader?importLoaders=1',
20 | 'sass-loader'
21 | ]
22 | },
23 | ]
24 | },
25 | optimization: {
26 | minimizer: [
27 | new OptimizeCssAssetsPlugin(),
28 | new UglifyJsPlugin({
29 | cache: true,
30 | parallel: true,
31 | sourceMap: true
32 | })
33 | ]
34 | }
35 | })
36 |
--------------------------------------------------------------------------------
/assets/js/components/MaterialFormSelect.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Select from '@material-ui/core/Select'
3 | import { asField } from 'informed'
4 |
5 | const MaterialFormSelect = asField(({ fieldState, fieldApi, ...props }) => {
6 | const {
7 | value
8 | } = fieldState
9 | const {
10 | setValue,
11 | setTouched
12 | } = fieldApi
13 | const {
14 | onChange,
15 | onBlur,
16 | forwardedRef,
17 | ...rest
18 | } = props
19 | return (
20 |