├── .coveragerc ├── .dockerignore ├── .github └── workflows │ ├── gh-pages.yml │ └── run-tests.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── coalesce ├── __init__.py ├── config │ ├── __init__.py │ ├── common.py │ ├── local.py │ └── production.py ├── opportunities │ ├── __init__.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20201127_1439.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── test │ │ ├── __init__.py │ │ ├── factories.py │ │ ├── test_serializers.py │ │ └── test_views.py │ └── views.py ├── organizations │ ├── __init__.py │ ├── admin.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── test │ │ ├── __init__.py │ │ ├── factories.py │ │ ├── test_serializers.py │ │ └── test_views.py │ └── views.py ├── organizers │ ├── __init__.py │ ├── admin.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_organizer_organization.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── test │ │ ├── __init__.py │ │ ├── factories.py │ │ └── test_views.py │ └── views.py ├── training_details │ ├── __init__.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── test │ │ ├── __init__.py │ │ ├── factories.py │ │ ├── test_serializers.py │ │ └── test_views.py │ └── views.py ├── urls.py ├── users │ ├── __init__.py │ ├── admin.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20171227_2246.py │ │ ├── 0003_auto_20210421_1503.py │ │ └── __init__.py │ ├── models.py │ ├── permissions.py │ ├── serializers.py │ ├── test │ │ ├── __init__.py │ │ ├── factories.py │ │ ├── test_serializers.py │ │ └── test_views.py │ └── views.py ├── volunteers │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20210421_0911.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── test │ │ ├── __init__.py │ │ ├── factories.py │ │ └── test_views.py │ └── views.py └── wsgi.py ├── docker-compose.yml ├── docs ├── api │ ├── authentication.md │ └── users.md ├── images │ ├── Dashboard.png │ ├── Oppertunity Applied.png │ ├── Oppertunity.png │ ├── Search.png │ └── Volunteer Profile.png └── index.md ├── frontend └── coalesce │ ├── .babelrc │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── .postcssrc.js │ ├── Dockerfile │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── jsconfig.json │ ├── package.json │ ├── public │ ├── favicon.ico │ └── icons │ │ ├── favicon-128x128.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon-96x96.png │ ├── quasar.conf.js │ ├── quasar.extensions.json │ ├── quasar.testing.json │ ├── src │ ├── App.vue │ ├── assets │ │ ├── coalesce-logo.png │ │ └── quasar-logo-full.svg │ ├── boot │ │ ├── .gitkeep │ │ ├── axios.js │ │ └── i18n.js │ ├── components │ │ ├── Banner.vue │ │ ├── EssentialLink.vue │ │ └── create-organiser │ │ │ ├── __tests__ │ │ │ └── create-organiser-form.test.js │ │ │ ├── create-organiser-form.vue │ │ │ └── utils │ │ │ └── form-data.js │ ├── css │ │ ├── app.sass │ │ └── quasar.variables.sass │ ├── i18n │ │ ├── en-us │ │ │ └── index.js │ │ └── index.js │ ├── index.template.html │ ├── layouts │ │ └── MainLayout.vue │ ├── pages │ │ ├── CreateOpportunity.vue │ │ ├── CreateOrganiser.vue │ │ ├── Error404.vue │ │ ├── Index.vue │ │ ├── LogIn.vue │ │ ├── OpportunitiesSearch.vue │ │ ├── OpportunityDetails.vue │ │ ├── OrganiserDashboard.vue │ │ └── VolunteerDetail.vue │ ├── router │ │ ├── index.js │ │ └── routes.js │ └── store │ │ ├── auth │ │ ├── actions.js │ │ ├── getters.js │ │ ├── index.js │ │ ├── mutations.js │ │ └── state.js │ │ ├── index.js │ │ └── store-flag.d.ts │ ├── test │ └── jest │ │ ├── .eslintrc.js │ │ ├── __tests__ │ │ ├── App.spec.js │ │ └── demo │ │ │ └── QBtn-demo.vue │ │ └── jest.setup.js │ └── yarn.lock ├── manage.py ├── mkdocs.yml ├── requirements.txt ├── run-api-tests.sh ├── setup.cfg ├── start_api.sh └── wait_for_postgres.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = *migrations*, 4 | *urls*, 5 | *test*, 6 | *admin*, 7 | ./manage.py, 8 | ./coalesce/config/*, 9 | ./coalesce/wsgi.py, 10 | *__init__* 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.coveragerc 3 | !.env 4 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: gh-pages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | - name: Generate documentation HTML 14 | run: python3 -m venv . && bin/pip install mkdocs && bin/mkdocs build 15 | - name: Deploy to GitHub Pages 16 | if: success() 17 | uses: crazy-max/ghaction-github-pages@v2 18 | with: 19 | target_branch: gh-pages 20 | build_dir: site 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | run-tests: 9 | runs-on: ubuntu-latest 10 | container: python:3.8 11 | 12 | services: 13 | db: 14 | image: postgres:latest 15 | env: 16 | POSTGRES_DB: test 17 | POSTGRES_USER: postgres 18 | POSTGRES_PASSWORD: password 19 | # Set health checks to wait until postgres has started 20 | options: >- 21 | --health-cmd pg_isready 22 | --health-interval 10s 23 | --health-timeout 5s 24 | --health-retries 5 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v2 29 | 30 | - name: Install dependencies 31 | run: pip install -r requirements.txt 32 | 33 | - name: Run flake8 to check our code follows the python style guide 34 | run: flake8 35 | 36 | - name: Run tests 37 | run: ./manage.py test 38 | env: 39 | DJANGO_SECRET_KEY: local 40 | DATABASE_URL: postgresql://postgres:password@db/test 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | .static_storage/ 56 | .media/ 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # pipenv 76 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 77 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 78 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 79 | # install all needed dependencies. 80 | #Pipfile.lock 81 | 82 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 83 | __pypackages__/ 84 | 85 | # Celery stuff 86 | celerybeat-schedule 87 | celerybeat.pid 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # django staticfiles 112 | /static 113 | 114 | # mypy 115 | .mypy_cache/ 116 | 117 | # editors 118 | *.code-workspace 119 | *.vscode 120 | .idea 121 | 122 | .DS_Store 123 | frontend/.DS_Store 124 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | ENV PYTHONUNBUFFERED 1 3 | 4 | # Allows docker to cache installed dependencies between builds 5 | COPY ./requirements.txt requirements.txt 6 | RUN pip install -r requirements.txt 7 | 8 | # Adds our application code to the image 9 | COPY . code 10 | WORKDIR code 11 | 12 | EXPOSE 8000 13 | 14 | # Run the production server 15 | CMD gunicorn --bind 0.0.0.0:$PORT --access-logfile - coalesce.wsgi:application 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coalesce 2 | 3 | [![Build Status](https://travis-ci.org/FederationOfTech/coalesce.svg?branch=master)](https://travis-ci.org/FederationOfTech/coalesce) 4 | [![Built with](https://img.shields.io/badge/Built_with-Cookiecutter_Django_Rest-F7B633.svg)](https://github.com/agconti/cookiecutter-django-rest) 5 | 6 | Coalesce is a 100% open-source volunteer management platform. 7 | Its mission is to make recruiting, onboarding, and managing volunteers as easy as possible. 8 | For volunteers, it is made to make finding tasks where you can contribute increadibly simple, allowing you to focus on contributing to the cause that means the most to you. 9 | 10 | Coalesce is a [Federation of Humanitarian Technologists](https://federationof.tech) member project. 11 | For more information on the work we do and how to work with us, please see our website. 12 | 13 | Some features you can expect from Coalesce are: 14 | 15 | - :mag: Easy browsing of existing volunteer opportunities 16 | - :loudspeaker: Register new events & recruit volunteers 17 | - :ballot_box_with_check: Check in volunteers at live events 18 | - :mage_woman: Generate reports on volunteer activities 19 | 20 | 21 | # Prerequisites 22 | 23 | Docker 24 | - Available on [Mac](https://docs.docker.com/docker-for-mac/install/) or [Windows](https://docs.docker.com/docker-for-windows/install/) 25 | - Windows client requires a specific version of [Windows](https://superuser.com/questions/1550291/how-to-install-windows-10-home-19018-update), [WSL 2 Linux kernel](https://docs.microsoft.com/en-gb/windows/wsl/wsl2-kernel) and [Git](https://git-scm.com/download/win). Once up and running Docker can be found on the right of the tool bar. Run `git config core.autocrlf false` before checking out a git branch so that UNIX line-endings needed by containers are used. 26 | 27 | # Local Development 28 | 29 | Coalesce is a web application which uses [Django](https://www.djangoproject.com/), [Django Rest Framework](https://www.django-rest-framework.org/), and Postgres on our backend. 30 | 31 | For our front end code, we use [Vue.js](https://vuejs.org/) 32 | 33 | To start the dev server for local development: 34 | ```bash 35 | docker-compose up 36 | ``` 37 | 38 | Once everything is running, you should be able to see: 39 | 40 | - the api server at [http://localhost:8000/](http://localhost:8000/), 41 | - the frontend at [http://localhost:8080/](http://localhost:8080/), 42 | - the documentation at [http://localhost:8001/](http://localhost:8001/). 43 | 44 | Run a command inside the api backend docker container: 45 | 46 | ```bash 47 | docker-compose exec api [command] 48 | ``` 49 | 50 | For example, `[command]` can be `/bin/bash` and you get a shell to the running container. 51 | 52 | Once you are in the container, you can then run the tests by doing: 53 | 54 | ```bash 55 | ./manage.py test 56 | ``` 57 | 58 | You can also start the django shell by doing: 59 | 60 | ```bash 61 | ./manage.py shell 62 | ``` 63 | 64 | To connect to the postgresql database, you can either do: 65 | 66 | ```bash 67 | docker-compose exec postgres psql -U postgres 68 | ``` 69 | 70 | or connect to `localhost` on port `5432` using a local postgresql client. 71 | 72 | # Documentation 73 | 74 | Check out the project's [documentation](https://FederationOfTech.github.io/Coalesce/). 75 | -------------------------------------------------------------------------------- /coalesce/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/__init__.py -------------------------------------------------------------------------------- /coalesce/config/__init__.py: -------------------------------------------------------------------------------- 1 | from .local import Local # noqa 2 | from .production import Production # noqa 3 | -------------------------------------------------------------------------------- /coalesce/config/common.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join 3 | from distutils.util import strtobool 4 | import dj_database_url 5 | from configurations import Configuration 6 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | 8 | 9 | class Common(Configuration): 10 | 11 | INSTALLED_APPS = ( 12 | 'django.contrib.admin', 13 | 'django.contrib.auth', 14 | 'django.contrib.contenttypes', 15 | 'django.contrib.sessions', 16 | 'django.contrib.messages', 17 | 'django.contrib.staticfiles', 18 | 'django.contrib.postgres', 19 | 20 | 21 | # Third party apps 22 | 'rest_framework', # utilities for rest apis 23 | 'rest_framework.authtoken', # token authentication 24 | 'django_filters', # for filtering rest endpoints 25 | 26 | # Your apps 27 | 'coalesce.users', 28 | 'coalesce.opportunities', 29 | 'coalesce.organizers', 30 | 'coalesce.organizations', 31 | 'coalesce.volunteers', 32 | 'coalesce.training_details' 33 | 34 | ) 35 | 36 | # https://docs.djangoproject.com/en/2.0/topics/http/middleware/ 37 | MIDDLEWARE = ( 38 | 'django.middleware.security.SecurityMiddleware', 39 | 'django.contrib.sessions.middleware.SessionMiddleware', 40 | 'django.middleware.common.CommonMiddleware', 41 | 'django.middleware.csrf.CsrfViewMiddleware', 42 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 43 | 'django.contrib.messages.middleware.MessageMiddleware', 44 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 45 | ) 46 | 47 | ALLOWED_HOSTS = ["*"] 48 | ROOT_URLCONF = 'coalesce.urls' 49 | SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') 50 | WSGI_APPLICATION = 'coalesce.wsgi.application' 51 | 52 | # Email 53 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 54 | 55 | ADMINS = ( 56 | ('Author', 'mike@federationof.tech'), 57 | ) 58 | 59 | # Postgres 60 | DATABASES = { 61 | # Can be overridden using DATABASE_URL 62 | 'default': dj_database_url.config( 63 | default='postgres://postgres:postgres@postgres:5432/postgres', 64 | conn_max_age=int(os.getenv('POSTGRES_CONN_MAX_AGE', 600)) 65 | ) 66 | } 67 | 68 | # General 69 | APPEND_SLASH = False 70 | TIME_ZONE = 'UTC' 71 | LANGUAGE_CODE = 'en-us' 72 | # If you set this to False, Django will make some optimizations so as not 73 | # to load the internationalization machinery. 74 | USE_I18N = False 75 | USE_L10N = True 76 | USE_TZ = True 77 | LOGIN_REDIRECT_URL = '/' 78 | 79 | # Static files (CSS, JavaScript, Images) 80 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 81 | STATIC_ROOT = os.path.normpath(join(os.path.dirname(BASE_DIR), 'static')) 82 | STATICFILES_DIRS = [] 83 | STATIC_URL = '/static/' 84 | STATICFILES_FINDERS = ( 85 | 'django.contrib.staticfiles.finders.FileSystemFinder', 86 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 87 | ) 88 | 89 | # Media files 90 | MEDIA_ROOT = join(os.path.dirname(BASE_DIR), 'media') 91 | MEDIA_URL = '/media/' 92 | 93 | TEMPLATES = [ 94 | { 95 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 96 | 'DIRS': STATICFILES_DIRS, 97 | 'APP_DIRS': True, 98 | 'OPTIONS': { 99 | 'context_processors': [ 100 | 'django.template.context_processors.debug', 101 | 'django.template.context_processors.request', 102 | 'django.contrib.auth.context_processors.auth', 103 | 'django.contrib.messages.context_processors.messages', 104 | ], 105 | }, 106 | }, 107 | ] 108 | 109 | # Set DEBUG to False as a default for safety 110 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 111 | DEBUG = strtobool(os.getenv('DJANGO_DEBUG', 'no')) 112 | 113 | # Password Validation 114 | # https://docs.djangoproject.com/en/2.0/topics/auth/passwords/#module-django.contrib.auth.password_validation 115 | AUTH_PASSWORD_VALIDATORS = [ 116 | { 117 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 118 | }, 119 | { 120 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 121 | }, 122 | { 123 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 124 | }, 125 | { 126 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 127 | }, 128 | ] 129 | 130 | # Logging 131 | LOGGING = { 132 | 'version': 1, 133 | 'disable_existing_loggers': False, 134 | 'formatters': { 135 | 'django.server': { 136 | '()': 'django.utils.log.ServerFormatter', 137 | 'format': '[%(server_time)s] %(message)s', 138 | }, 139 | 'verbose': { 140 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 141 | }, 142 | 'simple': { 143 | 'format': '%(levelname)s %(message)s' 144 | }, 145 | }, 146 | 'filters': { 147 | 'require_debug_true': { 148 | '()': 'django.utils.log.RequireDebugTrue', 149 | }, 150 | }, 151 | 'handlers': { 152 | 'django.server': { 153 | 'level': 'INFO', 154 | 'class': 'logging.StreamHandler', 155 | 'formatter': 'django.server', 156 | }, 157 | 'console': { 158 | 'level': 'DEBUG', 159 | 'class': 'logging.StreamHandler', 160 | 'formatter': 'simple' 161 | }, 162 | 'mail_admins': { 163 | 'level': 'ERROR', 164 | 'class': 'django.utils.log.AdminEmailHandler' 165 | } 166 | }, 167 | 'loggers': { 168 | 'django': { 169 | 'handlers': ['console'], 170 | 'propagate': True, 171 | }, 172 | 'django.server': { 173 | 'handlers': ['django.server'], 174 | 'level': 'INFO', 175 | 'propagate': False, 176 | }, 177 | 'django.request': { 178 | 'handlers': ['mail_admins', 'console'], 179 | 'level': 'ERROR', 180 | 'propagate': False, 181 | }, 182 | 'django.db.backends': { 183 | 'handlers': ['console'], 184 | 'level': 'INFO' 185 | }, 186 | } 187 | } 188 | 189 | # Custom user app 190 | AUTH_USER_MODEL = 'users.User' 191 | 192 | # Django Rest Framework 193 | REST_FRAMEWORK = { 194 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 195 | 'PAGE_SIZE': int(os.getenv('DJANGO_PAGINATION_LIMIT', 10)), 196 | 'DATETIME_FORMAT': '%Y-%m-%dT%H:%M:%S%z', 197 | 'DEFAULT_RENDERER_CLASSES': ( 198 | 'rest_framework.renderers.JSONRenderer', 199 | 'rest_framework.renderers.BrowsableAPIRenderer', 200 | ), 201 | 'DEFAULT_PERMISSION_CLASSES': [ 202 | 'rest_framework.permissions.IsAuthenticated', 203 | ], 204 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 205 | 'rest_framework.authentication.SessionAuthentication', 206 | 'rest_framework.authentication.TokenAuthentication', 207 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 208 | ) 209 | } 210 | 211 | CORS_ORIGIN_WHITELIST = ("localhost", "frontend") 212 | -------------------------------------------------------------------------------- /coalesce/config/local.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .common import Common 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | 5 | 6 | class Local(Common): 7 | DEBUG = True 8 | 9 | # Testing 10 | INSTALLED_APPS = Common.INSTALLED_APPS 11 | INSTALLED_APPS += ('django_nose',) 12 | TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 13 | NOSE_ARGS = [ 14 | BASE_DIR, 15 | '-s', 16 | '--nologcapture', 17 | '--with-coverage', 18 | '--with-progressive', 19 | '--cover-package=coalesce' 20 | ] 21 | 22 | # Mail 23 | EMAIL_HOST = 'localhost' 24 | EMAIL_PORT = 1025 25 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 26 | -------------------------------------------------------------------------------- /coalesce/config/production.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .common import Common 3 | 4 | 5 | class Production(Common): 6 | INSTALLED_APPS = Common.INSTALLED_APPS 7 | SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') 8 | # Site 9 | # https://docs.djangoproject.com/en/2.0/ref/settings/#allowed-hosts 10 | ALLOWED_HOSTS = ["*"] 11 | INSTALLED_APPS += ("gunicorn", ) 12 | 13 | # Static files (CSS, JavaScript, Images) 14 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 15 | # http://django-storages.readthedocs.org/en/latest/index.html 16 | INSTALLED_APPS += ('storages',) 17 | DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' 18 | STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' 19 | AWS_ACCESS_KEY_ID = os.getenv('DJANGO_AWS_ACCESS_KEY_ID') 20 | AWS_SECRET_ACCESS_KEY = os.getenv('DJANGO_AWS_SECRET_ACCESS_KEY') 21 | AWS_STORAGE_BUCKET_NAME = os.getenv('DJANGO_AWS_STORAGE_BUCKET_NAME') 22 | AWS_DEFAULT_ACL = 'public-read' 23 | AWS_AUTO_CREATE_BUCKET = True 24 | AWS_QUERYSTRING_AUTH = False 25 | MEDIA_URL = f'https://s3.amazonaws.com/{AWS_STORAGE_BUCKET_NAME}/' 26 | 27 | # https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#cache-control 28 | # Response can be cached by browser and any intermediary caches (i.e. it is "public") for up to 1 day 29 | # 86400 = (60 seconds x 60 minutes x 24 hours) 30 | AWS_HEADERS = { 31 | 'Cache-Control': 'max-age=86400, s-maxage=86400, must-revalidate', 32 | } 33 | -------------------------------------------------------------------------------- /coalesce/opportunities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/opportunities/__init__.py -------------------------------------------------------------------------------- /coalesce/opportunities/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.8 on 2020-11-27 13:43 2 | 3 | import django.contrib.postgres.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Opportunity', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('background_check_requirements', models.TextField()), 20 | ('commitment_type', models.TextField()), 21 | ('datetime', models.DateTimeField(blank=True, help_text='Event date')), 22 | ('description', models.TextField()), 23 | ('clothing_requirements', models.TextField()), 24 | ('location', models.TextField()), 25 | ('personnel_needed', models.IntegerField()), 26 | ('post_privacy', models.CharField(choices=[('public', 'public'), ('private', 'private'), ('unlisted', 'unlisted')], default='public', max_length=8)), 27 | ('skills_required', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=60), size=None)), 28 | ('title', models.CharField(max_length=60)), 29 | ], 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /coalesce/opportunities/migrations/0002_auto_20201127_1439.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.8 on 2020-11-27 14:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('organizers', '0001_initial'), 10 | ('opportunities', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='opportunity', 16 | name='organizers', 17 | field=models.ManyToManyField(to='organizers.Organizer'), 18 | ), 19 | migrations.AlterField( 20 | model_name='opportunity', 21 | name='clothing_requirements', 22 | field=models.TextField(blank=True), 23 | ), 24 | migrations.AlterField( 25 | model_name='opportunity', 26 | name='datetime', 27 | field=models.DateTimeField(), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /coalesce/opportunities/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/opportunities/migrations/__init__.py -------------------------------------------------------------------------------- /coalesce/opportunities/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.postgres.fields import ArrayField 3 | 4 | class Opportunity(models.Model): 5 | 6 | background_check_requirements = models.TextField() 7 | commitment_type = models.TextField() 8 | datetime = models.DateTimeField() 9 | description = models.TextField() 10 | clothing_requirements = models.TextField(blank=True) 11 | # TODO image = models.ChartField(max_length=30) # this should be ref to a stored image url 12 | location = models.TextField() 13 | organizers = models.ManyToManyField('organizers.Organizer') 14 | personnel_needed = models.IntegerField() 15 | post_privacy = models.CharField(choices=[ 16 | ("public", "public"), 17 | ("private", "private"), 18 | ("unlisted", "unlisted"), 19 | ], default="public", max_length=8) 20 | skills_required = ArrayField(models.CharField(max_length=60)) 21 | title = models.CharField(max_length=60) 22 | # TODO training_requirements = models.TextField() # this should ref training data type 23 | -------------------------------------------------------------------------------- /coalesce/opportunities/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Opportunity 3 | from ..organizers.models import Organizer 4 | 5 | class OpportunitySerializer(serializers.ModelSerializer): 6 | organizers = serializers.PrimaryKeyRelatedField( 7 | queryset=Organizer.objects.all(), 8 | many=True 9 | ) 10 | skills_required = serializers.ListField() 11 | 12 | class Meta: 13 | model = Opportunity 14 | fields = ('id', 15 | 'datetime', 16 | 'title', 17 | 'description', 18 | 'location', 19 | 'organizers', 20 | 'personnel_needed', 21 | 'skills_required', 22 | 'commitment_type', 23 | 'background_check_requirements', 24 | # TODO 'image', 25 | 'clothing_requirements', 26 | 'post_privacy', 27 | # TODO 'training_requirements' 28 | ) 29 | -------------------------------------------------------------------------------- /coalesce/opportunities/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/opportunities/test/__init__.py -------------------------------------------------------------------------------- /coalesce/opportunities/test/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | import datetime 3 | 4 | 5 | class OpportunityFactory(factory.django.DjangoModelFactory): 6 | 7 | class Meta: 8 | model = 'opportunities.Opportunity' 9 | 10 | background_check_requirements = "test-background-check-requirements" 11 | commitment_type = "test-commitements" 12 | datetime = datetime.datetime.fromisoformat("2020-11-26T10:50:31+00:00") 13 | description = "test-description" 14 | clothing_requirements = "test-clothing" 15 | id = 1 16 | location = "test-location" 17 | personnel_needed = 10 18 | post_privacy = "public" 19 | skills_required = ["test-skill-1", "test-skill-2"] 20 | title = "test-opportunity" 21 | # training_requirements = "test-training-requirements" 22 | -------------------------------------------------------------------------------- /coalesce/opportunities/test/test_serializers.py: -------------------------------------------------------------------------------- 1 | from django.forms.models import model_to_dict 2 | from django.test import TestCase 3 | from nose.tools import eq_, ok_ 4 | 5 | from ..serializers import OpportunitySerializer 6 | from .factories import OpportunityFactory 7 | 8 | 9 | class TestCreateOpportunitySerializer(TestCase): 10 | 11 | def setUp(self): 12 | self.opportunity_data = model_to_dict(OpportunityFactory.build()) 13 | 14 | def test_serializer_with_empty_data(self): 15 | serializer = OpportunitySerializer(data={}) 16 | eq_(serializer.is_valid(), False) 17 | 18 | def test_serializer_with_valid_data(self): 19 | serializer = OpportunitySerializer(data=self.opportunity_data) 20 | ok_(serializer.is_valid()) 21 | -------------------------------------------------------------------------------- /coalesce/opportunities/test/test_views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from nose.tools import eq_ 3 | from rest_framework.test import APITestCase 4 | from rest_framework import status 5 | 6 | import factory 7 | from .factories import OpportunityFactory 8 | from ..models import Opportunity 9 | from ...organizers.test.factories import OrganizerFactory 10 | from ...users.test.factories import UserFactory 11 | 12 | 13 | class TestOpportunitiesListTestCase(APITestCase): 14 | """ 15 | Tests /opportunities list operations. 16 | """ 17 | 18 | def setUp(self): 19 | self.url = reverse('opportunity-list') 20 | self.opportunity_data = factory.build( 21 | dict, FACTORY_CLASS=OpportunityFactory 22 | ) 23 | self.user = UserFactory() 24 | 25 | def test_anonymous_post_request_fails(self): 26 | response = self.client.post(self.url, {}) 27 | eq_(response.status_code, status.HTTP_403_FORBIDDEN) 28 | 29 | def test_post_request_with_no_data_fails(self): 30 | self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.user.auth_token}') 31 | response = self.client.post(self.url, {}) 32 | eq_(response.status_code, status.HTTP_400_BAD_REQUEST) 33 | 34 | def test_post_request_with_valid_data_succeeds(self): 35 | self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.user.auth_token}') 36 | response = self.client.post(self.url, self.opportunity_data) 37 | eq_(response.status_code, status.HTTP_201_CREATED) 38 | 39 | opportunity = Opportunity.objects.get(pk=response.data.get('id')) 40 | eq_(opportunity.title, self.opportunity_data.get('title')) 41 | eq_(opportunity.description, self.opportunity_data.get('description')) 42 | 43 | def test_post_request_with_query_params_returns_subset(self): 44 | self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.user.auth_token}') 45 | response = self.client.post(self.url, self.opportunity_data) 46 | eq_(response.status_code, status.HTTP_201_CREATED) 47 | 48 | opportunity = Opportunity.objects.get(pk=response.data.get('id')) 49 | eq_(opportunity.title, self.opportunity_data.get('title')) 50 | eq_(opportunity.description, self.opportunity_data.get('description')) 51 | 52 | search_url = self.url + "?search=test" 53 | response = self.client.get(search_url) 54 | 55 | eq_(1, len(response.data["results"])) 56 | opportunity = Opportunity.objects.get(pk=response.data["results"][0].get("id")) 57 | eq_(opportunity.title, self.opportunity_data.get('title')) 58 | eq_(opportunity.description, self.opportunity_data.get('description')) 59 | 60 | search_url = self.url + "?search=foo-no-results" 61 | response = self.client.get(search_url) 62 | eq_(0, len(response.data["results"])) 63 | 64 | 65 | class TestOpportunityDetailTestCase(APITestCase): 66 | """ 67 | Tests /opportunities detail operations. 68 | """ 69 | 70 | def setUp(self): 71 | self.opportunity = OpportunityFactory() 72 | self.url = reverse('opportunity-detail', kwargs={'pk': self.opportunity.pk}) 73 | 74 | # Creata an organizer and add them to the opportunity 75 | self.organizer = OrganizerFactory() 76 | self.opportunity.organizers.add(self.organizer) 77 | 78 | def test_get_request_returns_a_given_opportunity(self): 79 | response = self.client.get(self.url) 80 | eq_(response.status_code, status.HTTP_200_OK) 81 | 82 | def test_unauthenticated_patch_request_forbidden(self): 83 | new_title = "New title" 84 | payload = {'title': new_title} 85 | response = self.client.patch(self.url, payload) 86 | eq_(response.status_code, status.HTTP_403_FORBIDDEN) 87 | 88 | def test_wrong_user_patch_request_forbidden(self): 89 | other_user = OrganizerFactory() 90 | self.client.credentials(HTTP_AUTHORIZATION=f'Token {other_user.user.auth_token}') 91 | new_title = "New title" 92 | payload = {'title': new_title} 93 | response = self.client.patch(self.url, payload) 94 | eq_(response.status_code, status.HTTP_403_FORBIDDEN) 95 | 96 | def test_authenticated_patch_request_updates_an_opportunity(self): 97 | self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.organizer.user.auth_token}') 98 | new_title = "New title" 99 | payload = {'title': new_title} 100 | response = self.client.patch(self.url, payload) 101 | eq_(response.status_code, status.HTTP_200_OK) 102 | 103 | opportunity = Opportunity.objects.get(pk=self.opportunity.id) 104 | eq_(opportunity.title, new_title) 105 | 106 | def test_authenticated_patch_request_updates_organizers(self): 107 | self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.organizer.user.auth_token}') 108 | new_organizer = OrganizerFactory() 109 | payload = {'organizers': [new_organizer.pk]} 110 | response = self.client.patch(self.url, payload) 111 | eq_(response.status_code, status.HTTP_200_OK) 112 | 113 | opportunity = Opportunity.objects.get(pk=self.opportunity.id) 114 | organizers = opportunity.organizers.all() 115 | eq_(organizers.count(), 1) 116 | eq_(str(organizers[0].pk), new_organizer.pk) 117 | -------------------------------------------------------------------------------- /coalesce/opportunities/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets, mixins 2 | from rest_framework.permissions import BasePermission, SAFE_METHODS 3 | from rest_framework import filters 4 | 5 | from .models import Opportunity 6 | from .serializers import OpportunitySerializer 7 | 8 | 9 | class OpportunityOwnerPermission(BasePermission): 10 | """ 11 | Only allow users who appear in the object organizers list to submit PATCH requests 12 | """ 13 | 14 | def has_object_permission(self, request, view, obj): 15 | # Read permissions are allowed to any request, 16 | # so we'll always allow GET, HEAD or OPTIONS requests. 17 | if request.method in SAFE_METHODS: 18 | return True 19 | 20 | # For updates, the user must be listed as an organizer of the opportunity 21 | return request.user in [o.user for o in obj.organizers.all()] 22 | 23 | def has_permission(self, request, view): 24 | # To post a new object simply test if the user is authenticated 25 | # TODO: Also verify that the user is an organizer 26 | if request.method == 'POST': 27 | return request.user.is_authenticated 28 | return True 29 | 30 | 31 | class OpportunityViewSet(mixins.RetrieveModelMixin, 32 | mixins.UpdateModelMixin, 33 | mixins.CreateModelMixin, 34 | mixins.ListModelMixin, 35 | viewsets.GenericViewSet): 36 | """ 37 | Retrieves opportunities. 38 | """ 39 | queryset = Opportunity.objects.all() 40 | serializer_class = OpportunitySerializer 41 | filter_backends = [filters.SearchFilter] 42 | search_fields = ["@title", "@description"] 43 | permission_classes = (OpportunityOwnerPermission,) 44 | -------------------------------------------------------------------------------- /coalesce/organizations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/organizations/__init__.py -------------------------------------------------------------------------------- /coalesce/organizations/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Organization 3 | 4 | 5 | @admin.register(Organization) 6 | class OrganizationAdmin(admin.ModelAdmin): 7 | pass 8 | -------------------------------------------------------------------------------- /coalesce/organizations/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.8 on 2021-04-20 11:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Organization', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.TextField(help_text='Organization name')), 19 | ('description', models.TextField(help_text='Organization description')), 20 | ('website', models.TextField(help_text='Organization website')), 21 | ('org_type', models.TextField(help_text='Organization type')), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /coalesce/organizations/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/organizations/migrations/__init__.py -------------------------------------------------------------------------------- /coalesce/organizations/models.py: -------------------------------------------------------------------------------- 1 | 2 | from django.db import models 3 | 4 | 5 | class Organization(models.Model): 6 | '''An Organization is a collection of organizers, joined via foreign key in the organizer model''' 7 | 8 | # Organization metadata 9 | name = models.TextField(help_text='Organization name') 10 | description = models.TextField(help_text='Organization description') 11 | website = models.TextField(help_text='Organization website') 12 | org_type = models.TextField(help_text='Organization type') 13 | 14 | def __str__(self): 15 | return f'Organization: {self.name}' 16 | -------------------------------------------------------------------------------- /coalesce/organizations/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Organization 3 | 4 | 5 | class OrganizationSerializer(serializers.ModelSerializer): 6 | 7 | class Meta: 8 | model = Organization 9 | fields = ('id', 10 | 'name', 11 | 'description', 12 | 'website', 13 | 'org_type' 14 | ) 15 | -------------------------------------------------------------------------------- /coalesce/organizations/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/organizations/test/__init__.py -------------------------------------------------------------------------------- /coalesce/organizations/test/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | 4 | class OrganizationFactory(factory.django.DjangoModelFactory): 5 | 6 | class Meta: 7 | model = 'organizations.Organization' 8 | 9 | name = "Combined Charity" 10 | description = "This is a great charity" 11 | website = "www.google.com" 12 | org_type = "Charity" 13 | -------------------------------------------------------------------------------- /coalesce/organizations/test/test_serializers.py: -------------------------------------------------------------------------------- 1 | from django.forms.models import model_to_dict 2 | from django.test import TestCase 3 | from nose.tools import eq_, ok_ 4 | 5 | from ..serializers import OrganizationSerializer 6 | from .factories import OrganizationFactory 7 | 8 | 9 | class TestCreateOpportunitySerializer(TestCase): 10 | 11 | def setUp(self): 12 | self.opportunity_data = model_to_dict(OrganizationFactory.build()) 13 | 14 | def test_serializer_with_empty_data(self): 15 | serializer = OrganizationSerializer(data={}) 16 | eq_(serializer.is_valid(), False) 17 | 18 | def test_serializer_with_valid_data(self): 19 | serializer = OrganizationSerializer(data=self.opportunity_data) 20 | ok_(serializer.is_valid()) 21 | -------------------------------------------------------------------------------- /coalesce/organizations/test/test_views.py: -------------------------------------------------------------------------------- 1 | 2 | import factory 3 | 4 | from django.urls import reverse 5 | from nose.tools import eq_ 6 | from rest_framework.test import APITestCase 7 | from rest_framework import status 8 | from ..models import Organization 9 | from ...organizations.test.factories import OrganizationFactory 10 | 11 | 12 | class TestOrganizationCreate(APITestCase): 13 | """ 14 | Tests /organizers create operations. 15 | """ 16 | 17 | def setUp(self): 18 | self.organization_data = factory.build( 19 | dict, FACTORY_CLASS=OrganizationFactory 20 | ) 21 | self.url = reverse('organization-list') 22 | 23 | def test_post_request_with_valid_data_succeeds(self): 24 | response = self.client.post(self.url, self.organization_data) 25 | eq_(response.status_code, status.HTTP_201_CREATED) 26 | 27 | organization = Organization.objects.get(pk=response.data.get('id')) 28 | eq_(organization.name, self.organization_data.get('name')) 29 | -------------------------------------------------------------------------------- /coalesce/organizations/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets, mixins 2 | from rest_framework.permissions import AllowAny 3 | 4 | from .models import Organization 5 | from .serializers import OrganizationSerializer 6 | 7 | 8 | class OrganizationViewSet(mixins.RetrieveModelMixin, 9 | mixins.UpdateModelMixin, 10 | mixins.CreateModelMixin, 11 | mixins.ListModelMixin, 12 | viewsets.GenericViewSet): 13 | """ 14 | Updates and retrieves opportunities. 15 | """ 16 | queryset = Organization.objects.all() 17 | serializer_class = OrganizationSerializer 18 | permission_classes = (AllowAny,) 19 | -------------------------------------------------------------------------------- /coalesce/organizers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/organizers/__init__.py -------------------------------------------------------------------------------- /coalesce/organizers/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Organizer 3 | 4 | 5 | @admin.register(Organizer) 6 | class OrganizerAdmin(admin.ModelAdmin): 7 | pass 8 | -------------------------------------------------------------------------------- /coalesce/organizers/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.8 on 2020-11-26 13:08 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('users', '0002_auto_20171227_2246'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Organizer', 19 | fields=[ 20 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /coalesce/organizers/migrations/0002_organizer_organization.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.8 on 2021-04-20 11:38 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('organizations', '0001_initial'), 11 | ('organizers', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='organizer', 17 | name='organization', 18 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='organizations.Organization'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /coalesce/organizers/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/organizers/migrations/__init__.py -------------------------------------------------------------------------------- /coalesce/organizers/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | 4 | 5 | class Organizer(models.Model): 6 | '''An Organizer is a User who manages Opportunities.''' 7 | user = models.OneToOneField( 8 | settings.AUTH_USER_MODEL, 9 | on_delete=models.CASCADE, 10 | primary_key=True 11 | ) 12 | 13 | organization = models.ForeignKey('organizations.Organization', on_delete=models.CASCADE, null=True) 14 | 15 | # The list of Opportunity objects that this Organizer manages is 16 | # 'opportunity_set'. It's the backwards ManyToMany relation for 17 | # Opportunity.organizers. 18 | 19 | def __str__(self): 20 | return f'Organizer {self.user}' 21 | -------------------------------------------------------------------------------- /coalesce/organizers/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Organizer 3 | 4 | 5 | class CreateOrganizerSerializer(serializers.ModelSerializer): 6 | def create(self, validated_data): 7 | return Organizer.objects.create(user=self.context['request'].user, 8 | **validated_data) 9 | 10 | class Meta: 11 | model = Organizer 12 | fields = ('user', 'organization') 13 | read_only_fields = ('user',) 14 | -------------------------------------------------------------------------------- /coalesce/organizers/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/organizers/test/__init__.py -------------------------------------------------------------------------------- /coalesce/organizers/test/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from ...users.test.factories import UserFactory 3 | 4 | 5 | class OrganizerFactory(factory.django.DjangoModelFactory): 6 | 7 | class Meta: 8 | model = 'organizers.Organizer' 9 | 10 | user = factory.SubFactory(UserFactory) 11 | -------------------------------------------------------------------------------- /coalesce/organizers/test/test_views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from nose.tools import eq_ 3 | from rest_framework.test import APITestCase 4 | from rest_framework import status 5 | from ...users.test.factories import UserFactory 6 | 7 | 8 | class TestOrganizerCreate(APITestCase): 9 | """ 10 | Tests /organizers create operations. 11 | """ 12 | 13 | def setUp(self): 14 | self.user = UserFactory() 15 | self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.user.auth_token}') 16 | self.url = reverse('organizer-list') 17 | 18 | def test_post_request_with_valid_data_succeeds(self): 19 | response = self.client.post(self.url, {}) 20 | eq_(response.status_code, status.HTTP_201_CREATED) 21 | 22 | eq_(str(response.data.get('user')), self.user.id) 23 | -------------------------------------------------------------------------------- /coalesce/organizers/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets, mixins 2 | from .models import Organizer 3 | from rest_framework.permissions import IsAuthenticated 4 | from .serializers import CreateOrganizerSerializer 5 | 6 | 7 | class OrganizerCreateViewSet(mixins.CreateModelMixin, 8 | viewsets.GenericViewSet): 9 | """ 10 | Creates organizers 11 | """ 12 | queryset = Organizer.objects.all() 13 | serializer_class = CreateOrganizerSerializer 14 | permission_classes = (IsAuthenticated,) 15 | -------------------------------------------------------------------------------- /coalesce/training_details/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/training_details/__init__.py -------------------------------------------------------------------------------- /coalesce/training_details/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TrainingDetailsConfig(AppConfig): 5 | name = 'training_details' 6 | -------------------------------------------------------------------------------- /coalesce/training_details/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.8 on 2020-11-27 16:00 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='TrainingDetails', 17 | fields=[ 18 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 19 | ('name', models.TextField(help_text='Name of the training')), 20 | ('link', models.URLField(help_text='Link to online training course')), 21 | ('date', models.DateField(help_text='Date of the training')), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /coalesce/training_details/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/training_details/migrations/__init__.py -------------------------------------------------------------------------------- /coalesce/training_details/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.db import models 4 | 5 | 6 | class TrainingDetails(models.Model): 7 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 8 | name = models.TextField(help_text='Name of the training') 9 | link = models.URLField(help_text='Link to online training course') 10 | date = models.DateField(help_text='Date of the training') 11 | -------------------------------------------------------------------------------- /coalesce/training_details/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import TrainingDetails 3 | 4 | 5 | class CreateTrainingDetailsSerializer(serializers.ModelSerializer): 6 | 7 | def create(self, validated_data): 8 | return TrainingDetails.objects.create(**validated_data) 9 | 10 | class Meta: 11 | model = TrainingDetails 12 | fields = ('id', 'name', 'link', 'date',) 13 | 14 | 15 | class TrainingDetailsSerializer(serializers.ModelSerializer): 16 | class Meta: 17 | model = TrainingDetails 18 | fields = ('id', 'name', 'link', 'date',) 19 | -------------------------------------------------------------------------------- /coalesce/training_details/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/training_details/test/__init__.py -------------------------------------------------------------------------------- /coalesce/training_details/test/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | import datetime 3 | 4 | 5 | class TrainingDetailsFactory(factory.django.DjangoModelFactory): 6 | class Meta: 7 | model = 'training_details.TrainingDetails' 8 | 9 | id = factory.Faker('uuid4') 10 | name = 'Test Training' 11 | link = 'https://www.federationof.tech/' 12 | date = datetime.date.fromisoformat('2020-11-27') 13 | -------------------------------------------------------------------------------- /coalesce/training_details/test/test_serializers.py: -------------------------------------------------------------------------------- 1 | from django.forms.models import model_to_dict 2 | from django.test import TestCase 3 | from nose.tools import eq_, ok_ 4 | 5 | from ..serializers import TrainingDetailsSerializer 6 | from .factories import TrainingDetailsFactory 7 | 8 | 9 | class TestTrainingDetailsSerializer(TestCase): 10 | 11 | def setUp(self): 12 | self.opportunity_data = model_to_dict(TrainingDetailsFactory.build()) 13 | 14 | def test_serializer_with_empty_data(self): 15 | serializer = TrainingDetailsSerializer(data={}) 16 | eq_(serializer.is_valid(), False) 17 | 18 | def test_serializer_with_valid_data(self): 19 | serializer = TrainingDetailsSerializer(data=self.opportunity_data) 20 | ok_(serializer.is_valid()) 21 | -------------------------------------------------------------------------------- /coalesce/training_details/test/test_views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.urls import reverse 3 | from nose.tools import eq_ 4 | from rest_framework.test import APITestCase 5 | from rest_framework import status 6 | from ...users.test.factories import UserFactory 7 | from .factories import TrainingDetailsFactory 8 | from ..models import TrainingDetails 9 | 10 | 11 | class TestTrainingDetailsCreate(APITestCase): 12 | """ 13 | Tests /training_details create operations 14 | """ 15 | 16 | def setUp(self): 17 | self.user = UserFactory() 18 | self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.user.auth_token}') 19 | self.url = reverse('trainingdetails-list') 20 | self.training_details_data = { 21 | 'name': 'Test Training', 22 | 'link': 'https://www.federationof.tech/', 23 | 'date': datetime.date.fromisoformat('2020-11-27'), 24 | } 25 | 26 | def test_post_request_with_no_data_fails(self): 27 | response = self.client.post(self.url, {}) 28 | eq_(response.status_code, status.HTTP_400_BAD_REQUEST) 29 | 30 | def test_post_request_with_valid_data_succeeds(self): 31 | response = self.client.post(self.url, self.training_details_data) 32 | eq_(response.status_code, status.HTTP_201_CREATED) 33 | 34 | training_details = TrainingDetails.objects.get(pk=response.data.get('id')) 35 | eq_(response.data.get('id'), str(training_details.id)) 36 | eq_(response.data.get('name'), training_details.name) 37 | eq_(response.data.get('link'), training_details.link) 38 | eq_(datetime.date.fromisoformat(response.data.get('date')), training_details.date) 39 | 40 | 41 | class TestTrainingDetailsDetail(APITestCase): 42 | """ 43 | Tests /training_details detail operations 44 | """ 45 | 46 | def setUp(self): 47 | self.user = UserFactory() 48 | self.training_details = TrainingDetailsFactory() 49 | self.url = reverse('trainingdetails-detail', kwargs={'pk': self.training_details.pk}) 50 | 51 | def test_get_request_with_no_authorization_fails(self): 52 | response = self.client.get(self.url) 53 | eq_(response.status_code, status.HTTP_403_FORBIDDEN) 54 | 55 | def test_get_request_with_authorization_succeeds(self): 56 | self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.user.auth_token}') 57 | response = self.client.get(self.url) 58 | eq_(response.status_code, status.HTTP_200_OK) 59 | eq_(response.data.get('id'), self.training_details.id) 60 | eq_(response.data.get('name'), self.training_details.name) 61 | eq_(response.data.get('link'), self.training_details.link) 62 | eq_(datetime.date.fromisoformat(response.data.get('date')), self.training_details.date) 63 | -------------------------------------------------------------------------------- /coalesce/training_details/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets, mixins 2 | from rest_framework.permissions import IsAuthenticated 3 | 4 | from .models import TrainingDetails 5 | from .serializers import TrainingDetailsSerializer, CreateTrainingDetailsSerializer 6 | 7 | 8 | class TrainingDetailsCreateViewSet(mixins.CreateModelMixin, 9 | viewsets.GenericViewSet): 10 | queryset = TrainingDetails.objects.all() 11 | serializer_class = CreateTrainingDetailsSerializer 12 | permission_classes = (IsAuthenticated,) 13 | 14 | 15 | class TrainingDetailsViewSet(mixins.RetrieveModelMixin, 16 | viewsets.GenericViewSet): 17 | queryset = TrainingDetails.objects.all() 18 | serializer_class = TrainingDetailsSerializer 19 | permission_classes = (IsAuthenticated,) 20 | -------------------------------------------------------------------------------- /coalesce/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.urls import path, re_path, include, reverse_lazy 3 | from django.conf.urls.static import static 4 | from django.contrib import admin 5 | from django.views.generic.base import RedirectView 6 | from rest_framework.routers import DefaultRouter 7 | from rest_framework_simplejwt.views import ( 8 | TokenObtainPairView, 9 | TokenRefreshView, 10 | ) 11 | from .opportunities.views import OpportunityViewSet 12 | from .users.views import UserViewSet, UserCreateViewSet 13 | from .organizers.views import OrganizerCreateViewSet 14 | from .organizations.views import OrganizationViewSet 15 | from .volunteers.views import VolunteerCreateViewSet, VolunteerViewSet 16 | from .training_details.views import TrainingDetailsViewSet, TrainingDetailsCreateViewSet 17 | 18 | router = DefaultRouter() 19 | router.register(r"users", UserViewSet) 20 | router.register(r"users", UserCreateViewSet) 21 | router.register(r"opportunities", OpportunityViewSet) 22 | router.register(r"organizers", OrganizerCreateViewSet) 23 | router.register(r"organizations", OrganizationViewSet) 24 | router.register(r"volunteers", VolunteerCreateViewSet) 25 | router.register(r"volunteers", VolunteerViewSet) 26 | router.register(r"training_details", TrainingDetailsViewSet) 27 | router.register(r"training_details", TrainingDetailsCreateViewSet) 28 | 29 | urlpatterns = [ 30 | path("admin/", admin.site.urls), 31 | path("api/v1/", include(router.urls)), 32 | path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), 33 | path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), 34 | path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), 35 | # the 'api-root' from django rest-frameworks default router 36 | # http://www.django-rest-framework.org/api-guide/routers/#defaultrouter 37 | re_path(r"^$", RedirectView.as_view(url=reverse_lazy("api-root"), permanent=False)), 38 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 39 | -------------------------------------------------------------------------------- /coalesce/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/users/__init__.py -------------------------------------------------------------------------------- /coalesce/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from .models import User 4 | 5 | 6 | @admin.register(User) 7 | class UserAdmin(UserAdmin): 8 | pass 9 | -------------------------------------------------------------------------------- /coalesce/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.8 on 2017-12-21 03:04 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.auth.models 6 | import django.contrib.auth.validators 7 | from django.db import migrations, models 8 | import django.utils.timezone 9 | import uuid 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | initial = True 15 | 16 | dependencies = [ 17 | ('auth', '0008_alter_user_username_max_length'), 18 | ] 19 | 20 | operations = [ 21 | migrations.CreateModel( 22 | name='User', 23 | fields=[ 24 | ('password', models.CharField(max_length=128, verbose_name='password')), 25 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 26 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 27 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 28 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 29 | ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), 30 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 31 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 32 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 33 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 34 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 35 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 36 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 37 | ], 38 | options={ 39 | 'verbose_name': 'user', 40 | 'verbose_name_plural': 'users', 41 | 'abstract': False, 42 | }, 43 | managers=[ 44 | ('objects', django.contrib.auth.models.UserManager()), 45 | ], 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /coalesce/users/migrations/0002_auto_20171227_2246.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2017-12-27 22:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='last_name', 16 | field=models.CharField(blank=True, max_length=150, verbose_name='last name'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /coalesce/users/migrations/0003_auto_20210421_1503.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.8 on 2021-04-21 15:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0002_auto_20171227_2246'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='first_name', 16 | field=models.TextField(), 17 | ), 18 | migrations.AlterField( 19 | model_name='user', 20 | name='last_name', 21 | field=models.TextField(), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /coalesce/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/users/migrations/__init__.py -------------------------------------------------------------------------------- /coalesce/users/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from django.db import models 3 | from django.conf import settings 4 | from django.dispatch import receiver 5 | from django.contrib.auth.models import AbstractUser 6 | from django.db.models.signals import post_save 7 | from rest_framework.authtoken.models import Token 8 | 9 | 10 | class User(AbstractUser): 11 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 12 | first_name = models.TextField() 13 | last_name = models.TextField() 14 | 15 | def __str__(self): 16 | return self.username 17 | 18 | 19 | @receiver(post_save, sender=settings.AUTH_USER_MODEL) 20 | def create_auth_token(sender, instance=None, created=False, **kwargs): 21 | if created: 22 | Token.objects.create(user=instance) 23 | -------------------------------------------------------------------------------- /coalesce/users/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsUserOrReadOnly(permissions.BasePermission): 5 | """ 6 | Object-level permission to only allow owners of an object to edit it. 7 | """ 8 | 9 | def has_object_permission(self, request, view, obj): 10 | 11 | if request.method in permissions.SAFE_METHODS: 12 | return True 13 | 14 | return obj == request.user 15 | -------------------------------------------------------------------------------- /coalesce/users/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import User 3 | 4 | 5 | class UserSerializer(serializers.ModelSerializer): 6 | 7 | class Meta: 8 | model = User 9 | fields = ('id', 'username', 'first_name', 'last_name',) 10 | read_only_fields = ('username', ) 11 | 12 | 13 | class CreateUserSerializer(serializers.ModelSerializer): 14 | 15 | def create(self, validated_data): 16 | # call create_user on user object. Without this 17 | # the password will be stored in plain text. 18 | user = User.objects.create_user(**validated_data) 19 | return user 20 | 21 | class Meta: 22 | model = User 23 | fields = ('id', 'username', 'password', 'first_name', 'last_name', 'email', 'auth_token',) 24 | read_only_fields = ('auth_token',) 25 | extra_kwargs = {'password': {'write_only': True}} 26 | -------------------------------------------------------------------------------- /coalesce/users/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/users/test/__init__.py -------------------------------------------------------------------------------- /coalesce/users/test/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | 4 | class UserFactory(factory.django.DjangoModelFactory): 5 | 6 | class Meta: 7 | model = 'users.User' 8 | django_get_or_create = ('username',) 9 | 10 | id = factory.Faker('uuid4') 11 | username = factory.Sequence(lambda n: f'testuser{n}') 12 | password = factory.Faker('password', length=10, special_chars=True, digits=True, 13 | upper_case=True, lower_case=True) 14 | email = factory.Faker('email') 15 | first_name = factory.Faker('first_name') 16 | last_name = factory.Faker('last_name') 17 | is_active = True 18 | is_staff = False 19 | -------------------------------------------------------------------------------- /coalesce/users/test/test_serializers.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.forms.models import model_to_dict 3 | from django.contrib.auth.hashers import check_password 4 | from nose.tools import eq_, ok_ 5 | from .factories import UserFactory 6 | from ..serializers import CreateUserSerializer 7 | 8 | 9 | class TestCreateUserSerializer(TestCase): 10 | 11 | def setUp(self): 12 | self.user_data = model_to_dict(UserFactory.build()) 13 | 14 | def test_serializer_with_empty_data(self): 15 | serializer = CreateUserSerializer(data={}) 16 | eq_(serializer.is_valid(), False) 17 | 18 | def test_serializer_with_valid_data(self): 19 | serializer = CreateUserSerializer(data=self.user_data) 20 | ok_(serializer.is_valid()) 21 | 22 | def test_serializer_hashes_password(self): 23 | serializer = CreateUserSerializer(data=self.user_data) 24 | ok_(serializer.is_valid()) 25 | 26 | user = serializer.save() 27 | ok_(check_password(self.user_data.get('password'), user.password)) 28 | -------------------------------------------------------------------------------- /coalesce/users/test/test_views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.contrib.auth.hashers import check_password 3 | from nose.tools import ok_, eq_ 4 | from rest_framework.test import APITestCase 5 | from rest_framework import status 6 | from faker import Faker 7 | import factory 8 | from ..models import User 9 | from .factories import UserFactory 10 | 11 | fake = Faker() 12 | 13 | 14 | class TestUserListTestCase(APITestCase): 15 | """ 16 | Tests /users list operations. 17 | """ 18 | 19 | def setUp(self): 20 | self.url = reverse('user-list') 21 | self.user_data = factory.build(dict, FACTORY_CLASS=UserFactory) 22 | 23 | def test_post_request_with_no_data_fails(self): 24 | response = self.client.post(self.url, {}) 25 | eq_(response.status_code, status.HTTP_400_BAD_REQUEST) 26 | 27 | def test_post_request_with_valid_data_succeeds(self): 28 | response = self.client.post(self.url, self.user_data) 29 | eq_(response.status_code, status.HTTP_201_CREATED) 30 | 31 | user = User.objects.get(pk=response.data.get('id')) 32 | eq_(user.username, self.user_data.get('username')) 33 | ok_(check_password(self.user_data.get('password'), user.password)) 34 | 35 | 36 | class TestUserDetailTestCase(APITestCase): 37 | """ 38 | Tests /users detail operations. 39 | """ 40 | 41 | def setUp(self): 42 | self.user = UserFactory() 43 | self.url = reverse('user-detail', kwargs={'pk': self.user.pk}) 44 | self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.user.auth_token}') 45 | 46 | def test_get_request_returns_a_given_user(self): 47 | response = self.client.get(self.url) 48 | eq_(response.status_code, status.HTTP_200_OK) 49 | 50 | def test_put_request_updates_a_user(self): 51 | new_first_name = fake.first_name() 52 | new_last_name = fake.last_name() 53 | payload = {'first_name': new_first_name, 'last_name': new_last_name} 54 | response = self.client.put(self.url, payload) 55 | eq_(response.status_code, status.HTTP_200_OK) 56 | 57 | user = User.objects.get(pk=self.user.id) 58 | eq_(user.first_name, new_first_name) 59 | -------------------------------------------------------------------------------- /coalesce/users/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets, mixins 2 | from rest_framework.permissions import AllowAny 3 | from .models import User 4 | from .permissions import IsUserOrReadOnly 5 | from .serializers import CreateUserSerializer, UserSerializer 6 | 7 | 8 | class UserViewSet(mixins.RetrieveModelMixin, 9 | mixins.UpdateModelMixin, 10 | viewsets.GenericViewSet): 11 | """ 12 | Updates and retrieves user accounts 13 | """ 14 | queryset = User.objects.all() 15 | serializer_class = UserSerializer 16 | permission_classes = (IsUserOrReadOnly,) 17 | 18 | 19 | class UserCreateViewSet(mixins.CreateModelMixin, 20 | viewsets.GenericViewSet): 21 | """ 22 | Creates user accounts 23 | """ 24 | queryset = User.objects.all() 25 | serializer_class = CreateUserSerializer 26 | permission_classes = (AllowAny,) 27 | -------------------------------------------------------------------------------- /coalesce/volunteers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/volunteers/__init__.py -------------------------------------------------------------------------------- /coalesce/volunteers/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Volunteer 3 | 4 | 5 | @admin.register(Volunteer) 6 | class VolunteerAdmin(admin.ModelAdmin): 7 | pass 8 | -------------------------------------------------------------------------------- /coalesce/volunteers/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class VolunteersConfig(AppConfig): 5 | name = 'volunteers' 6 | -------------------------------------------------------------------------------- /coalesce/volunteers/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.8 on 2020-11-26 15:27 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('users', '0002_auto_20171227_2246'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Volunteer', 19 | fields=[ 20 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), 21 | ('description', models.TextField()), 22 | ('skills', models.TextField(help_text='List of skills the volunteer has')), 23 | ('organization', models.TextField(help_text="The volunteer's employer or organization")), 24 | ('organizer_comments', models.TextField()), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /coalesce/volunteers/migrations/0002_auto_20210421_0911.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.8 on 2021-04-21 09:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('volunteers', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='volunteer', 15 | name='organizer_comments', 16 | field=models.TextField(help_text='Notes from organizers about this volunteer'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /coalesce/volunteers/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/volunteers/migrations/__init__.py -------------------------------------------------------------------------------- /coalesce/volunteers/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | 4 | 5 | class Volunteer(models.Model): 6 | user = models.OneToOneField( 7 | settings.AUTH_USER_MODEL, 8 | on_delete=models.CASCADE, 9 | primary_key=True 10 | ) 11 | description = models.TextField() 12 | # TODO List of Opportunities they have indicated interest in 13 | skills = models.TextField(help_text='List of skills the volunteer has') # TODO is this just a string? 14 | organization = models.TextField(help_text='The volunteer\'s employer or organization') 15 | # TODO Background check date and document 16 | organizer_comments = models.TextField(help_text='Notes from organizers about this volunteer') 17 | -------------------------------------------------------------------------------- /coalesce/volunteers/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Volunteer 3 | 4 | class CreateVolunteerSerializer(serializers.ModelSerializer): 5 | 6 | def create(self, validated_data): 7 | return Volunteer.objects.create(user=self.context['request'].user, 8 | **validated_data) 9 | 10 | class Meta: 11 | model = Volunteer 12 | fields = ('description', 'skills', 'organization', 'user',) 13 | read_only_fields = ('user',) 14 | 15 | class VolunteerDetailsSerializer(serializers.ModelSerializer): 16 | 17 | class Meta: 18 | model = Volunteer 19 | fields = ('description', 'skills', 'organization', 'user',) 20 | -------------------------------------------------------------------------------- /coalesce/volunteers/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/coalesce/volunteers/test/__init__.py -------------------------------------------------------------------------------- /coalesce/volunteers/test/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from ...users.test.factories import UserFactory 3 | 4 | 5 | class VolunteerFactory(factory.django.DjangoModelFactory): 6 | 7 | class Meta: 8 | model = 'volunteers.Volunteer' 9 | django_get_or_create = ('user',) 10 | 11 | user = factory.SubFactory(UserFactory) 12 | description = 'I am a volunteer' 13 | skills = 'cooking, eating' 14 | organization = 'Camden Giving' 15 | -------------------------------------------------------------------------------- /coalesce/volunteers/test/test_views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from nose.tools import eq_ 3 | from rest_framework.test import APITestCase 4 | from rest_framework import status 5 | from ...users.test.factories import UserFactory 6 | from .factories import VolunteerFactory 7 | from ..models import Volunteer 8 | 9 | 10 | class TestVolunteerCreate(APITestCase): 11 | """ 12 | Tests /volunteers create operations. 13 | """ 14 | 15 | def setUp(self): 16 | self.user = UserFactory() 17 | self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.user.auth_token}') 18 | self.url = reverse('volunteer-list') 19 | self.user_data = { 20 | 'description': 'I am a volunteer.', 21 | 'skills': 'cooking, eating', 22 | 'organization': 'Cambden Giving', 23 | } 24 | 25 | def test_post_request_with_no_data_fails(self): 26 | response = self.client.post(self.url, {}) 27 | eq_(response.status_code, status.HTTP_400_BAD_REQUEST) 28 | 29 | def test_post_request_with_valid_data_succeeds(self): 30 | response = self.client.post(self.url, self.user_data) 31 | eq_(response.status_code, status.HTTP_201_CREATED) 32 | 33 | volunteer = Volunteer.objects.get(pk=response.data.get('user')) 34 | eq_(str(response.data.get('user')), self.user.id) 35 | eq_(response.data.get('description'), volunteer.description) 36 | eq_(response.data.get('skills'), volunteer.skills) 37 | eq_(response.data.get('organization'), volunteer.organization) 38 | 39 | 40 | class TestVolunteerDetails(APITestCase): 41 | """ 42 | Tests /volunteers details operations. 43 | """ 44 | 45 | def setUp(self): 46 | self.volunteer = VolunteerFactory() 47 | self.url = reverse('volunteer-detail', kwargs={'pk': self.volunteer.pk}) 48 | 49 | def test_get_request_with_no_authorization_fails(self): 50 | response = self.client.get(self.url) 51 | eq_(response.status_code, status.HTTP_403_FORBIDDEN) 52 | 53 | def test_get_request_with_authorization_succeeds(self): 54 | self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.volunteer.user.auth_token}') 55 | response = self.client.get(self.url) 56 | eq_(response.status_code, status.HTTP_200_OK) 57 | eq_(str(response.data.get('user')), self.volunteer.user.id) 58 | eq_(response.data.get('description'), self.volunteer.description) 59 | eq_(response.data.get('skills'), self.volunteer.skills) 60 | eq_(response.data.get('organization'), self.volunteer.organization) 61 | -------------------------------------------------------------------------------- /coalesce/volunteers/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets, mixins 2 | from rest_framework.permissions import IsAuthenticated 3 | from .models import Volunteer 4 | from .serializers import CreateVolunteerSerializer, VolunteerDetailsSerializer 5 | 6 | 7 | class VolunteerCreateViewSet(mixins.CreateModelMixin, 8 | viewsets.GenericViewSet): 9 | """ 10 | Creates volunteers 11 | """ 12 | queryset = Volunteer.objects.all() 13 | serializer_class = CreateVolunteerSerializer 14 | permission_classes = (IsAuthenticated,) 15 | 16 | 17 | class VolunteerViewSet(mixins.RetrieveModelMixin, 18 | viewsets.GenericViewSet): 19 | """ 20 | Retrieves volunteers 21 | """ 22 | queryset = Volunteer.objects.all() 23 | serializer_class = VolunteerDetailsSerializer 24 | permission_classes = (IsAuthenticated,) 25 | -------------------------------------------------------------------------------- /coalesce/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for coalesce project. 3 | It exposes the WSGI callable as a module-level variable named ``application``. 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/gunicorn/ 6 | """ 7 | import os 8 | 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "coalesce.config") 10 | os.environ.setdefault("DJANGO_CONFIGURATION", "Production") 11 | 12 | from configurations.wsgi import get_wsgi_application # noqa 13 | application = get_wsgi_application() 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | postgres: 5 | image: postgres:latest 6 | environment: 7 | - POSTGRES_DB=postgres 8 | - POSTGRES_USER=postgres 9 | - POSTGRES_PASSWORD=postgres 10 | ports: 11 | - "5432:5432" 12 | api: 13 | restart: always 14 | environment: 15 | - DJANGO_SECRET_KEY=local 16 | - DATABASE_URL=postgresql://postgres:postgres@postgres/postgres 17 | image: api 18 | build: ./ 19 | command: > 20 | bash -c "python wait_for_postgres.py && 21 | ./manage.py migrate && 22 | ./manage.py runserver 0.0.0.0:8000" 23 | volumes: 24 | - ./:/code:z 25 | ports: 26 | - "8000:8000" 27 | depends_on: 28 | - postgres 29 | links: 30 | - postgres:postgres 31 | frontend: 32 | image: frontend 33 | build: ./frontend/coalesce/ 34 | command: quasar dev 35 | ports: 36 | - "8080:8080" 37 | depends_on: 38 | - api 39 | links: 40 | - api:api 41 | volumes: 42 | - ./frontend/coalesce/src/:/app/src/:z 43 | - ./frontend/coalesce/quasar.conf.js:/app/quasar.conf.js:z 44 | documentation: 45 | restart: always 46 | build: ./ 47 | command: "mkdocs serve" 48 | volumes: 49 | - ./:/code:z 50 | ports: 51 | - "8001:8001" 52 | -------------------------------------------------------------------------------- /docs/api/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | For clients to authenticate, the token key should be included in the Authorization HTTP header. The key should be prefixed by the string literal "Token", with whitespace separating the two strings. For example: 3 | 4 | ``` 5 | Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b 6 | ``` 7 | 8 | Unauthenticated responses that are denied permission will result in an HTTP `401 Unauthorized` response with an appropriate `WWW-Authenticate` header. For example: 9 | 10 | ``` 11 | WWW-Authenticate: Token 12 | ``` 13 | 14 | The curl command line tool may be useful for testing token authenticated APIs. For example: 15 | 16 | ```bash 17 | curl -X GET http://127.0.0.1:8000/api/v1/example/ -H 'Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' 18 | ``` 19 | 20 | ## Retrieving Tokens 21 | Authorization tokens are issued and returned when a user registers. A registered user can also retrieve their token with the following request: 22 | 23 | **Request**: 24 | 25 | `POST` `api-token-auth/` 26 | 27 | Parameters: 28 | 29 | Name | Type | Description 30 | ---|---|--- 31 | username | string | The user's username 32 | password | string | The user's password 33 | 34 | **Response**: 35 | ```json 36 | { 37 | "token" : "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/api/users.md: -------------------------------------------------------------------------------- 1 | # Users 2 | Supports registering, viewing, and updating user accounts. 3 | 4 | ## Register a new user account 5 | 6 | **Request**: 7 | 8 | `POST` `/users/` 9 | 10 | Parameters: 11 | 12 | Name | Type | Required | Description 13 | -----------|--------|----------|------------ 14 | username | string | Yes | The username for the new user. 15 | password | string | Yes | The password for the new user account. 16 | first_name | string | No | The user's given name. 17 | last_name | string | No | The user's family name. 18 | email | string | No | The user's email address. 19 | 20 | *Note:* 21 | 22 | - Not Authorization Protected 23 | 24 | **Response**: 25 | 26 | ```json 27 | Content-Type application/json 28 | 201 Created 29 | 30 | { 31 | "id": "6d5f9bae-a31b-4b7b-82c4-3853eda2b011", 32 | "username": "richard", 33 | "first_name": "Richard", 34 | "last_name": "Hendriks", 35 | "email": "richard@piedpiper.com", 36 | "auth_token": "132cf952e0165a274bf99e115ab483671b3d9ff6" 37 | } 38 | ``` 39 | 40 | The `auth_token` returned with this response should be stored by the client for 41 | authenticating future requests to the API. See [Authentication](authentication.md). 42 | 43 | 44 | ## Get a user's profile information 45 | 46 | **Request**: 47 | 48 | `GET` `/users/:id` 49 | 50 | Parameters: 51 | 52 | *Note:* 53 | 54 | - **[Authorization Protected](authentication.md)** 55 | 56 | **Response**: 57 | 58 | ```json 59 | Content-Type application/json 60 | 200 OK 61 | 62 | { 63 | "id": "6d5f9bae-a31b-4b7b-82c4-3853eda2b011", 64 | "username": "richard", 65 | "first_name": "Richard", 66 | "last_name": "Hendriks", 67 | "email": "richard@piedpiper.com", 68 | } 69 | ``` 70 | 71 | 72 | ## Update your profile information 73 | 74 | **Request**: 75 | 76 | `PUT/PATCH` `/users/:id` 77 | 78 | Parameters: 79 | 80 | Name | Type | Description 81 | -----------|--------|--- 82 | first_name | string | The first_name of the user object. 83 | last_name | string | The last_name of the user object. 84 | email | string | The user's email address. 85 | 86 | 87 | 88 | *Note:* 89 | 90 | - All parameters are optional 91 | - **[Authorization Protected](authentication.md)** 92 | 93 | **Response**: 94 | 95 | ```json 96 | Content-Type application/json 97 | 200 OK 98 | 99 | { 100 | "id": "6d5f9bae-a31b-4b7b-82c4-3853eda2b011", 101 | "username": "richard", 102 | "first_name": "Richard", 103 | "last_name": "Hendriks", 104 | "email": "richard@piedpiper.com", 105 | } 106 | ``` 107 | -------------------------------------------------------------------------------- /docs/images/Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/docs/images/Dashboard.png -------------------------------------------------------------------------------- /docs/images/Oppertunity Applied.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/docs/images/Oppertunity Applied.png -------------------------------------------------------------------------------- /docs/images/Oppertunity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/docs/images/Oppertunity.png -------------------------------------------------------------------------------- /docs/images/Search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/docs/images/Search.png -------------------------------------------------------------------------------- /docs/images/Volunteer Profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/docs/images/Volunteer Profile.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # coalesce 2 | 3 | [![Build Status](https://travis-ci.org/FederationOfTech/coalesce.svg?branch=master)](https://travis-ci.org/FederationOfTech/coalesce) 4 | [![Built with](https://img.shields.io/badge/Built_with-Cookiecutter_Django_Rest-F7B633.svg)](https://github.com/agconti/cookiecutter-django-rest) 5 | 6 | An open source volunteer management platform. Check out the project's [documentation](https://FederationOfTech.github.io/Coalesce/). 7 | 8 | # Prerequisites 9 | 10 | - [Docker](https://docs.docker.com/docker-for-mac/install/) 11 | 12 | # Initialize the project 13 | 14 | Start the dev server for local development: 15 | 16 | ```bash 17 | docker-compose up 18 | ``` 19 | 20 | Once everything is running, you should be able to see: 21 | 22 | - the api server at [http://localhost:8000/](http://localhost:8000/), 23 | - the frontend at [http://localhost:8080/](http://localhost:8080/), 24 | - the documentation at [http://localhost:8001/](http://localhost:8001/). 25 | 26 | Create a superuser to login to the admin: 27 | 28 | ```bash 29 | docker-compose exec api ./manage.py createsuperuser 30 | ``` 31 | 32 | Run a command inside the api backend docker container: 33 | 34 | ```bash 35 | docker-compose exec api [command] 36 | ``` 37 | 38 | For example, `[command]` can be `/bin/bash` and you get a shell to the running container. 39 | 40 | Once you are in the container, you can then run the tests by doing: 41 | 42 | ```bash 43 | ./manage.py test 44 | ``` 45 | 46 | You can also start the django shell by doing: 47 | 48 | ```bash 49 | ./manage.py shell 50 | ``` 51 | 52 | To connect to the postgresql database, you can either do: 53 | 54 | ```bash 55 | docker-compose exec postgres psql -U postgres 56 | ``` 57 | 58 | or connect to `localhost` on port `5432` using a local postgresql client. 59 | -------------------------------------------------------------------------------- /frontend/coalesce/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-syntax-dynamic-import"], 3 | "env": { 4 | "test": { 5 | "plugins": ["dynamic-import-node"], 6 | "presets": [ 7 | [ 8 | "@babel/preset-env", 9 | { 10 | "modules": "commonjs", 11 | "targets": { 12 | "node": "current" 13 | } 14 | } 15 | ] 16 | ] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/coalesce/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /frontend/coalesce/.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /src-bex/www 3 | /src-capacitor 4 | /src-cordova 5 | /.quasar 6 | /node_modules 7 | -------------------------------------------------------------------------------- /frontend/coalesce/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy 3 | // This option interrupts the configuration hierarchy at this file 4 | // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos) 5 | root: true, 6 | 7 | parserOptions: { 8 | parser: 'babel-eslint', 9 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 10 | sourceType: 'module' // Allows for the use of imports 11 | }, 12 | 13 | env: { 14 | browser: true, 15 | "jest/globals": true 16 | }, 17 | 18 | // Rules order is important, please avoid shuffling them 19 | extends: [ 20 | // Base ESLint recommended rules 21 | // 'eslint:recommended', 22 | 23 | 24 | // Uncomment any of the lines below to choose desired strictness, 25 | // but leave only one uncommented! 26 | // See https://eslint.vuejs.org/rules/#available-rules 27 | 'plugin:vue/essential', // Priority A: Essential (Error Prevention) 28 | // 'plugin:vue/strongly-recommended', // Priority B: Strongly Recommended (Improving Readability) 29 | // 'plugin:vue/recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) 30 | 31 | 'standard', 32 | 'plugin:jest/recommended' 33 | 34 | ], 35 | 36 | plugins: [ 37 | // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file 38 | // required to lint *.vue files 39 | 'vue', 40 | 41 | ], 42 | 43 | globals: { 44 | ga: true, // Google Analytics 45 | cordova: true, 46 | __statics: true, 47 | process: true, 48 | Capacitor: true, 49 | chrome: true 50 | }, 51 | 52 | // add your custom rules here 53 | rules: { 54 | // allow async-await 55 | 'generator-star-spacing': 'off', 56 | // allow paren-less arrow functions 57 | 'arrow-parens': 'off', 58 | 'one-var': 'off', 59 | "camelcase": "off", 60 | 61 | 'import/first': 'off', 62 | 'import/named': 'error', 63 | 'import/namespace': 'error', 64 | 'import/default': 'error', 65 | 'import/export': 'error', 66 | 'import/extensions': 'off', 67 | 'import/no-unresolved': 'off', 68 | 'import/no-extraneous-dependencies': 'off', 69 | 'prefer-promise-reject-errors': 'off', 70 | 71 | 72 | // allow debugger during development only 73 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /frontend/coalesce/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .thumbs.db 3 | node_modules 4 | 5 | # Quasar core related directories 6 | .quasar 7 | /dist 8 | 9 | # Cordova related directories and files 10 | /src-cordova/node_modules 11 | /src-cordova/platforms 12 | /src-cordova/plugins 13 | /src-cordova/www 14 | 15 | # Capacitor related directories and files 16 | /src-capacitor/www 17 | /src-capacitor/node_modules 18 | 19 | # BEX related directories and files 20 | /src-bex/www 21 | /src-bex/js/core 22 | 23 | # Log files 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Editor directories and files 29 | .idea 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | -------------------------------------------------------------------------------- /frontend/coalesce/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | plugins: [ 5 | // to edit target browsers: use "browserslist" field in package.json 6 | require('autoprefixer') 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /frontend/coalesce/Dockerfile: -------------------------------------------------------------------------------- 1 | # develop stage 2 | FROM node:14-alpine as develop-stage 3 | 4 | WORKDIR /app 5 | COPY package*.json ./ 6 | COPY . /app 7 | 8 | RUN yarn global add @quasar/cli 9 | RUN yarn 10 | 11 | RUN npm rebuild node-sass # needed to run sass on alpine -------------------------------------------------------------------------------- /frontend/coalesce/README.md: -------------------------------------------------------------------------------- 1 | # Coalesce (coalesce) 2 | 3 | An open source volunteer management platform 4 | 5 | ## Install the dependencies 6 | ```bash 7 | yarn 8 | ``` 9 | 10 | ### Start the app in development mode (hot-code reloading, error reporting, etc.) 11 | ```bash 12 | quasar dev 13 | ``` 14 | 15 | ### Lint the files 16 | ```bash 17 | yarn run lint 18 | ``` 19 | 20 | ### Build the app for production 21 | ```bash 22 | quasar build 23 | ``` 24 | 25 | ### Customize the configuration 26 | See [Configuring quasar.conf.js](https://quasar.dev/quasar-cli/quasar-conf-js). 27 | -------------------------------------------------------------------------------- /frontend/coalesce/babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const fs = require('fs-extra') 3 | let extend 4 | 5 | /** 6 | * The .babelrc file has been created to assist Jest for transpiling. 7 | * You should keep your application's babel rules in this file. 8 | */ 9 | 10 | if (fs.existsSync('./.babelrc')) { 11 | extend = './.babelrc' 12 | } 13 | 14 | module.exports = { 15 | presets: ['@quasar/babel-preset-app'], 16 | extends: extend 17 | } 18 | -------------------------------------------------------------------------------- /frontend/coalesce/jest.config.js: -------------------------------------------------------------------------------- 1 | const esModules = ['quasar/lang', 'lodash-es'].join('|') 2 | 3 | module.exports = { 4 | globals: { 5 | __DEV__: true, 6 | // TODO: Remove if resolved natively https://github.com/vuejs/vue-jest/issues/175 7 | 'vue-jest': { 8 | pug: { doctype: 'html' } 9 | } 10 | }, 11 | setupFilesAfterEnv: ['/test/jest/jest.setup.js'], 12 | // noStackTrace: true, 13 | // bail: true, 14 | // cache: false, 15 | // verbose: true, 16 | // watch: true, 17 | collectCoverage: false, 18 | coverageDirectory: '/test/jest/coverage', 19 | collectCoverageFrom: [ 20 | '/src/**/*.vue', 21 | '/src/**/*.js', 22 | '/src/**/*.jsx' 23 | ], 24 | // Needed in JS codebases too because of feature flags 25 | coveragePathIgnorePatterns: ['/node_modules/', '.d.ts$'], 26 | coverageThreshold: { 27 | global: { 28 | // branches: 50, 29 | // functions: 50, 30 | // lines: 50, 31 | // statements: 50 32 | } 33 | }, 34 | testMatch: [ 35 | '/test/jest/__tests__/**/*.(spec|test).js', 36 | '/src/**/*.jest.(spec|test).js', 37 | '/src/**/*.(spec|test).js' 38 | ], 39 | moduleFileExtensions: ['vue', 'js', 'jsx', 'json'], 40 | moduleNameMapper: { 41 | '^vue$': 'vue/dist/vue.common.js', 42 | '^test-utils$': '@vue/test-utils/dist/vue-test-utils.js', 43 | '^quasar$': 'quasar/dist/quasar.common.js', 44 | '^~/(.*)$': '/$1', 45 | '^src/(.*)$': '/src/$1', 46 | '.*css$': '@quasar/quasar-app-extension-testing-unit-jest/stub.css' 47 | }, 48 | transform: { 49 | '.*\\.vue$': 'vue-jest', 50 | '.*\\.js$': 'babel-jest', 51 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 52 | 'jest-transform-stub' 53 | // use these if NPM is being flaky, care as hosting could interfere with these 54 | // '.*\\.vue$': '@quasar/quasar-app-extension-testing-unit-jest/node_modules/vue-jest', 55 | // '.*\\.js$': '@quasar/quasar-app-extension-testing-unit-jest/node_modules/babel-jest' 56 | }, 57 | transformIgnorePatterns: [`node_modules/(?!(${esModules}))`], 58 | snapshotSerializers: ['jest-serializer-vue'] 59 | } 60 | -------------------------------------------------------------------------------- /frontend/coalesce/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "src/*": [ 6 | "src/*" 7 | ], 8 | "app/*": [ 9 | "*" 10 | ], 11 | "components/*": [ 12 | "src/components/*" 13 | ], 14 | "layouts/*": [ 15 | "src/layouts/*" 16 | ], 17 | "pages/*": [ 18 | "src/pages/*" 19 | ], 20 | "assets/*": [ 21 | "src/assets/*" 22 | ], 23 | "boot/*": [ 24 | "src/boot/*" 25 | ], 26 | "vue$": [ 27 | "node_modules/vue/dist/vue.esm.js" 28 | ] 29 | } 30 | }, 31 | "exclude": [ 32 | "dist", 33 | ".quasar", 34 | "node_modules" 35 | ] 36 | } -------------------------------------------------------------------------------- /frontend/coalesce/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coalesce", 3 | "version": "0.0.1", 4 | "description": " An open source volunteer management platform ", 5 | "productName": "Coalesce", 6 | "author": "Mike Nolan ", 7 | "private": true, 8 | "scripts": { 9 | "lint": "eslint --ext .js,.vue ./", 10 | "test": "echo \"See package.json => scripts for available tests.\" && exit 0", 11 | "test:watch": "jest --watch", 12 | "storybook": "start-storybook -p 6006", 13 | "build-storybook": "build-storybook", 14 | "test:unit": "jest --updateSnapshot", 15 | "test:unit:ci": "jest --ci", 16 | "test:unit:coverage": "jest --coverage", 17 | "test:unit:watch": "jest --watch", 18 | "test:unit:watchAll": "jest --watchAll", 19 | "serve:test:coverage": "quasar serve test/jest/coverage/lcov-report/ --port 8788", 20 | "concurrently:dev:jest": "concurrently \"quasar dev\" \"jest --watch\"" 21 | }, 22 | "dependencies": { 23 | "@quasar/extras": "^1.0.0", 24 | "axios": "^0.21.0", 25 | "core-js": "^3.6.5", 26 | "jwt-decode": "^3.1.2", 27 | "quasar": "^1.0.0", 28 | "vue-i18n": "^8.0.0", 29 | "vuelidate": "^0.7.6" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.13.16", 33 | "@babel/preset-env": "^7.13.15", 34 | "@quasar/app": "^2.0.0", 35 | "@quasar/quasar-app-extension-testing-unit-jest": "^2.2.2", 36 | "@vue/test-utils": "^1.1.4", 37 | "babel-core": "^7.0.0-bridge.0", 38 | "babel-eslint": "^10.0.1", 39 | "babel-jest": "^26.6.3", 40 | "babel-loader": "^8.1.0", 41 | "eslint": "^6.8.0", 42 | "eslint-config-standard": "^14.1.0", 43 | "eslint-loader": "^3.0.3", 44 | "eslint-plugin-import": "^2.14.0", 45 | "eslint-plugin-jest": "^24.3.5", 46 | "eslint-plugin-node": "^11.0.0", 47 | "eslint-plugin-promise": "^4.0.1", 48 | "eslint-plugin-standard": "^4.0.0", 49 | "eslint-plugin-vue": "^6.1.2", 50 | "jest": "^26.6.3", 51 | "react-is": "^17.0.1", 52 | "vue-jest": "^3.0.7" 53 | }, 54 | "browserslist": [ 55 | "last 10 Chrome versions", 56 | "last 10 Firefox versions", 57 | "last 4 Edge versions", 58 | "last 7 Safari versions", 59 | "last 8 Android versions", 60 | "last 8 ChromeAndroid versions", 61 | "last 8 FirefoxAndroid versions", 62 | "last 10 iOS versions", 63 | "last 5 Opera versions" 64 | ], 65 | "engines": { 66 | "node": ">= 10.18.1", 67 | "npm": ">= 6.13.4", 68 | "yarn": ">= 1.21.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /frontend/coalesce/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/frontend/coalesce/public/favicon.ico -------------------------------------------------------------------------------- /frontend/coalesce/public/icons/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/frontend/coalesce/public/icons/favicon-128x128.png -------------------------------------------------------------------------------- /frontend/coalesce/public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/frontend/coalesce/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/coalesce/public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/frontend/coalesce/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/coalesce/public/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/frontend/coalesce/public/icons/favicon-96x96.png -------------------------------------------------------------------------------- /frontend/coalesce/quasar.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 3 | * the ES6 features that are supported by your Node version. https://node.green/ 4 | */ 5 | 6 | // Configuration for your app 7 | // https://quasar.dev/quasar-cli/quasar-conf-js 8 | /* eslint-env node */ 9 | 10 | module.exports = function (/* ctx */) { 11 | return { 12 | // https://quasar.dev/quasar-cli/supporting-ts 13 | supportTS: false, 14 | 15 | // https://quasar.dev/quasar-cli/prefetch-feature 16 | // preFetch: true, 17 | 18 | // app boot file (/src/boot) 19 | // --> boot files are part of "main.js" 20 | // https://quasar.dev/quasar-cli/boot-files 21 | boot: [ 22 | 23 | 'i18n', 24 | 'axios' 25 | ], 26 | 27 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css 28 | css: [ 29 | 'app.sass' 30 | ], 31 | 32 | // https://github.com/quasarframework/quasar/tree/dev/extras 33 | extras: [ 34 | // 'ionicons-v4', 35 | // 'mdi-v5', 36 | // 'fontawesome-v5', 37 | // 'eva-icons', 38 | // 'themify', 39 | // 'line-awesome', 40 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! 41 | 42 | 'roboto-font', // optional, you are not bound to it 43 | 'material-icons' // optional, you are not bound to it 44 | ], 45 | 46 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build 47 | build: { 48 | vueRouterMode: 'hash', // available values: 'hash', 'history' 49 | 50 | // transpile: false, 51 | 52 | // Add dependencies for transpiling with Babel (Array of string/regex) 53 | // (from node_modules, which are by default not transpiled). 54 | // Applies only if "transpile" is set to true. 55 | // transpileDependencies: [], 56 | 57 | // rtl: false, // https://quasar.dev/options/rtl-support 58 | // preloadChunks: true, 59 | // showProgress: false, 60 | // gzip: true, 61 | // analyze: true, 62 | 63 | // Options below are automatically set depending on the env, set them if you want to override 64 | // extractCSS: false, 65 | 66 | // https://quasar.dev/quasar-cli/handling-webpack 67 | extendWebpack (cfg) { 68 | cfg.module.rules.push({ 69 | enforce: 'pre', 70 | test: /\.(js|vue)$/, 71 | loader: 'eslint-loader', 72 | exclude: /node_modules/ 73 | }) 74 | } 75 | }, 76 | 77 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer 78 | devServer: { 79 | proxy: { 80 | '/api/': { 81 | target: { 82 | host: 'api', 83 | protocol: 'http:', 84 | port: 8000 85 | }, 86 | secure: false 87 | } 88 | }, 89 | https: false, 90 | port: 8080, 91 | open: true // opens browser window automatically 92 | }, 93 | 94 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework 95 | framework: { 96 | iconSet: 'material-icons', // Quasar icon set 97 | lang: 'en-us', // Quasar language pack 98 | config: {}, 99 | 100 | // Possible values for "importStrategy": 101 | // * 'auto' - (DEFAULT) Auto-import needed Quasar components & directives 102 | // * 'all' - Manually specify what to import 103 | importStrategy: 'auto', 104 | 105 | // For special cases outside of where "auto" importStrategy can have an impact 106 | // (like functional components as one of the examples), 107 | // you can manually specify Quasar components/directives to be available everywhere: 108 | // 109 | // components: [], 110 | // directives: [], 111 | 112 | // Quasar plugins 113 | plugins: ['Notify'] 114 | }, 115 | 116 | // animations: 'all', // --- includes all animations 117 | // https://quasar.dev/options/animations 118 | animations: [], 119 | 120 | // https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr 121 | ssr: { 122 | pwa: false 123 | }, 124 | 125 | // https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa 126 | pwa: { 127 | workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest' 128 | workboxOptions: {}, // only for GenerateSW 129 | manifest: { 130 | name: 'Coalesce', 131 | short_name: 'Coalesce', 132 | description: ' An open source volunteer management platform ', 133 | display: 'standalone', 134 | orientation: 'portrait', 135 | background_color: '#ffffff', 136 | theme_color: '#027be3', 137 | icons: [ 138 | { 139 | src: 'icons/icon-128x128.png', 140 | sizes: '128x128', 141 | type: 'image/png' 142 | }, 143 | { 144 | src: 'icons/icon-192x192.png', 145 | sizes: '192x192', 146 | type: 'image/png' 147 | }, 148 | { 149 | src: 'icons/icon-256x256.png', 150 | sizes: '256x256', 151 | type: 'image/png' 152 | }, 153 | { 154 | src: 'icons/icon-384x384.png', 155 | sizes: '384x384', 156 | type: 'image/png' 157 | }, 158 | { 159 | src: 'icons/icon-512x512.png', 160 | sizes: '512x512', 161 | type: 'image/png' 162 | } 163 | ] 164 | } 165 | }, 166 | 167 | // Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova 168 | cordova: { 169 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing 170 | }, 171 | 172 | // Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor 173 | capacitor: { 174 | hideSplashscreen: true 175 | }, 176 | 177 | // Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron 178 | electron: { 179 | bundler: 'packager', // 'packager' or 'builder' 180 | 181 | packager: { 182 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options 183 | 184 | // OS X / Mac App Store 185 | // appBundleId: '', 186 | // appCategoryType: '', 187 | // osxSign: '', 188 | // protocol: 'myapp://path', 189 | 190 | // Windows only 191 | // win32metadata: { ... } 192 | }, 193 | 194 | builder: { 195 | // https://www.electron.build/configuration/configuration 196 | 197 | appId: 'coalesce' 198 | }, 199 | 200 | // More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration 201 | nodeIntegration: true, 202 | 203 | extendWebpack (/* cfg */) { 204 | // do something with Electron main process Webpack cfg 205 | // chainWebpack also available besides this extendWebpack 206 | } 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /frontend/coalesce/quasar.extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "@quasar/testing-unit-jest": { 3 | "babel": "babelrc", 4 | "options": [ 5 | "scripts" 6 | ] 7 | } 8 | } -------------------------------------------------------------------------------- /frontend/coalesce/quasar.testing.json: -------------------------------------------------------------------------------- 1 | { 2 | "unit-jest": { 3 | "runnerCommand": "jest --ci" 4 | } 5 | } -------------------------------------------------------------------------------- /frontend/coalesce/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /frontend/coalesce/src/assets/coalesce-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/frontend/coalesce/src/assets/coalesce-logo.png -------------------------------------------------------------------------------- /frontend/coalesce/src/assets/quasar-logo-full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 63 | 66 | 69 | 75 | 79 | 83 | 87 | 91 | 95 | 99 | 103 | 104 | 105 | 106 | 107 | 113 | 118 | 126 | 133 | 142 | 151 | 160 | 169 | 178 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /frontend/coalesce/src/boot/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FederationOfTech/Coalesce/405ebfa8eab5ae6b69b094f26d422f4cf3f721c4/frontend/coalesce/src/boot/.gitkeep -------------------------------------------------------------------------------- /frontend/coalesce/src/boot/axios.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import axios from 'axios' 3 | 4 | Vue.prototype.$axios = axios 5 | -------------------------------------------------------------------------------- /frontend/coalesce/src/boot/i18n.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | import messages from 'src/i18n' 4 | 5 | Vue.use(VueI18n) 6 | 7 | const i18n = new VueI18n({ 8 | locale: 'en-us', 9 | fallbackLocale: 'en-us', 10 | messages 11 | }) 12 | 13 | export default ({ app }) => { 14 | // Set i18n instance on app 15 | app.i18n = i18n 16 | } 17 | 18 | export { i18n } 19 | -------------------------------------------------------------------------------- /frontend/coalesce/src/components/Banner.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 59 | 60 | 94 | -------------------------------------------------------------------------------- /frontend/coalesce/src/components/EssentialLink.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 50 | -------------------------------------------------------------------------------- /frontend/coalesce/src/components/create-organiser/__tests__/create-organiser-form.test.js: -------------------------------------------------------------------------------- 1 | import { mountQuasar } from '@quasar/quasar-app-extension-testing-unit-jest' 2 | import { QBtn, QForm, QInput, QRadio } from 'quasar' 3 | import OrganiserForm from '../create-organiser-form.vue' 4 | 5 | const $v = { 6 | websiteUrl: { 7 | $dirty: false, 8 | website: false 9 | }, 10 | organisationName: { 11 | $dirty: false 12 | }, 13 | email: { 14 | $dirty: false, 15 | email: false 16 | }, 17 | $touch: jest.fn() 18 | } 19 | 20 | describe('Create Organiser Form', () => { 21 | it('should be defined', () => { 22 | const wrapper = mountQuasar(OrganiserForm, { 23 | quasar: { 24 | components: { 25 | QBtn, 26 | QForm, 27 | QInput, 28 | QRadio 29 | } 30 | } 31 | }) 32 | 33 | expect(wrapper).toBeTruthy() 34 | 35 | expect(wrapper.exists()).toBe(true) 36 | }) 37 | 38 | it('should have one form with method "post"', () => { 39 | const wrapper = mountQuasar(OrganiserForm, { 40 | quasar: { 41 | components: { 42 | QBtn, 43 | QForm, 44 | QInput, 45 | QRadio 46 | } 47 | } 48 | }) 49 | 50 | expect(wrapper.attributes('method')).toBe('post') 51 | }) 52 | 53 | describe('methods ::', () => { 54 | describe('isformValid ::', () => { 55 | let touchSpy 56 | 57 | beforeEach(() => { 58 | touchSpy = jest.spyOn($v, '$touch') 59 | }) 60 | 61 | afterEach(() => { 62 | jest.clearAllMocks() 63 | }) 64 | 65 | it('should call `.touch()`', () => { 66 | const wrapper = mountQuasar(OrganiserForm, { 67 | quasar: { 68 | components: { 69 | QBtn, 70 | QForm, 71 | QInput, 72 | QRadio 73 | } 74 | }, 75 | mount: { 76 | mocks: { $v } 77 | } 78 | }) 79 | 80 | wrapper.vm.isFormValid() 81 | 82 | expect(touchSpy).toBeCalled() 83 | }) 84 | 85 | it('should return `true` if `$v` is valid', () => { 86 | $v.$invalid = false 87 | 88 | const wrapper = mountQuasar(OrganiserForm, { 89 | quasar: { 90 | components: { 91 | QBtn, 92 | QForm, 93 | QInput, 94 | QRadio 95 | } 96 | }, 97 | mount: { 98 | mocks: { $v } 99 | } 100 | }) 101 | 102 | expect(wrapper.vm.isFormValid()).toBeTruthy() 103 | }) 104 | 105 | it('should return `false` if `$v` is invalid', () => { 106 | $v.$invalid = true 107 | 108 | const wrapper = mountQuasar(OrganiserForm, { 109 | quasar: { 110 | components: { 111 | QBtn, 112 | QForm, 113 | QInput, 114 | QRadio 115 | } 116 | }, 117 | mount: { 118 | mocks: { $v } 119 | } 120 | }) 121 | 122 | expect(wrapper.vm.isFormValid()).toBeFalsy() 123 | }) 124 | }) 125 | 126 | describe('onFormSubmit', () => { 127 | let isFormValidSpy 128 | 129 | beforeEach(() => { 130 | isFormValidSpy = jest.spyOn(OrganiserForm.methods, 'isFormValid') 131 | }) 132 | 133 | it('should exist', () => { 134 | const wrapper = mountQuasar(OrganiserForm, { 135 | quasar: { 136 | components: { 137 | QBtn, 138 | QForm, 139 | QInput, 140 | QRadio 141 | } 142 | }, 143 | mount: { 144 | mocks: { $v } 145 | } 146 | }) 147 | 148 | expect(wrapper.vm.onFormSubmit).toBeDefined() 149 | }) 150 | 151 | describe('When invoked', () => { 152 | describe('And the form is not valid', () => { 153 | it('should not call `createOrganiser`', async () => { 154 | isFormValidSpy.mockReturnValue(false) 155 | const wrapper = mountQuasar(OrganiserForm, { 156 | quasar: { 157 | components: { 158 | QBtn, 159 | QForm, 160 | QInput, 161 | QRadio 162 | } 163 | }, 164 | mount: { 165 | mocks: { $v } 166 | } 167 | }) 168 | 169 | const createOrganiserSpy = jest.spyOn(wrapper.vm, 'createOrganiser') 170 | 171 | await wrapper.vm.onFormSubmit() 172 | 173 | expect(createOrganiserSpy).not.toHaveBeenCalled() 174 | }) 175 | }) 176 | describe('And the form is valid', () => { 177 | it('should call `createOrganiser`', async () => { 178 | isFormValidSpy.mockReturnValue(true) 179 | const wrapper = mountQuasar(OrganiserForm, { 180 | quasar: { 181 | components: { 182 | QBtn, 183 | QForm, 184 | QInput, 185 | QRadio 186 | } 187 | }, 188 | mount: { 189 | mocks: { $v } 190 | } 191 | }) 192 | 193 | const createOrganiserSpy = jest.spyOn(wrapper.vm, 'createOrganiser') 194 | 195 | await wrapper.vm.onFormSubmit() 196 | 197 | expect(createOrganiserSpy).toHaveBeenCalled() 198 | }) 199 | }) 200 | }) 201 | }) 202 | }) 203 | }) 204 | -------------------------------------------------------------------------------- /frontend/coalesce/src/components/create-organiser/create-organiser-form.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 101 | 102 | 157 | -------------------------------------------------------------------------------- /frontend/coalesce/src/components/create-organiser/utils/form-data.js: -------------------------------------------------------------------------------- 1 | export const organisationTypes = [ 2 | { 3 | label: 'Registered Charity', 4 | value: 'registered-charity' 5 | }, 6 | { 7 | label: 'Non-Registered Charity', 8 | value: 'non-registered-charity' 9 | }, 10 | { 11 | label: 'Social Enterprise', 12 | value: 'social-enterprise' 13 | }, 14 | { 15 | label: 'Non-For-Profit Organisation', 16 | value: 'non-for-profit' 17 | }, 18 | { 19 | label: 'Sports Club', 20 | value: 'sports-club' 21 | }, 22 | { 23 | label: 'Youth Club', 24 | value: 'youth-club' 25 | }, 26 | { 27 | label: 'Events Organiser', 28 | value: 'events-organiser' 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /frontend/coalesce/src/css/app.sass: -------------------------------------------------------------------------------- 1 | // app global css in Sass form 2 | 3 | .opportunity-card 4 | width: 100% 5 | 6 | #container 7 | position: relative 8 | 9 | #background 10 | position: absolute 11 | top: 0 12 | left: 0 13 | bottom: 0 14 | right: 0 15 | z-index: -1 16 | height: 210px 17 | overflow: hidden 18 | 19 | .card 20 | display: inline-block 21 | 22 | .icon 23 | text-align: center 24 | 25 | .centre-icon 26 | margin: auto 27 | width: 100% 28 | 29 | .small-icon 30 | font-size: 25px 31 | 32 | .large-icon 33 | font-size: 40px 34 | 35 | .search-bar 36 | width: 100% 37 | -------------------------------------------------------------------------------- /frontend/coalesce/src/css/quasar.variables.sass: -------------------------------------------------------------------------------- 1 | // Quasar Sass (& SCSS) Variables 2 | // -------------------------------------------------- 3 | // To customize the look and feel of this app, you can override 4 | // the Sass/SCSS variables found in Quasar's source Sass/SCSS files. 5 | 6 | // Check documentation for full list of Quasar variables 7 | 8 | // Your own variables (that are declared here) and Quasar's own 9 | // ones will be available out of the box in your .vue/.scss/.sass files 10 | 11 | // It's highly recommended to change the default colors 12 | // to match your app's branding. 13 | // Tip: Use the "Theme Builder" on Quasar's documentation website. 14 | 15 | $primary: #1976D2 16 | $secondary: #26A69A 17 | $accent: #F45C37 18 | 19 | $dark: #1D1D1D 20 | 21 | $positive: #21BA45 22 | $negative: #C10015 23 | $info: #31CCEC 24 | $warning: #F2C037 25 | 26 | $grey: #F3F3F3 27 | $font-primary: #2A4263 28 | -------------------------------------------------------------------------------- /frontend/coalesce/src/i18n/en-us/index.js: -------------------------------------------------------------------------------- 1 | // This is just an example, 2 | // so you can safely delete all default props below 3 | 4 | export default { 5 | failed: 'Action failed', 6 | success: 'Action was successful' 7 | } 8 | -------------------------------------------------------------------------------- /frontend/coalesce/src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import enUS from './en-us' 2 | 3 | export default { 4 | 'en-us': enUS 5 | } 6 | -------------------------------------------------------------------------------- /frontend/coalesce/src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= productName %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /frontend/coalesce/src/layouts/MainLayout.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 46 | 47 | 64 | -------------------------------------------------------------------------------- /frontend/coalesce/src/pages/CreateOpportunity.vue: -------------------------------------------------------------------------------- 1 | 122 | 123 | 210 | -------------------------------------------------------------------------------- /frontend/coalesce/src/pages/CreateOrganiser.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /frontend/coalesce/src/pages/Error404.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /frontend/coalesce/src/pages/Index.vue: -------------------------------------------------------------------------------- 1 | 112 | 113 | 118 | 119 | 137 | -------------------------------------------------------------------------------- /frontend/coalesce/src/pages/LogIn.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 81 | 82 | 87 | -------------------------------------------------------------------------------- /frontend/coalesce/src/pages/OpportunitiesSearch.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 122 | -------------------------------------------------------------------------------- /frontend/coalesce/src/pages/OpportunityDetails.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 117 | -------------------------------------------------------------------------------- /frontend/coalesce/src/pages/OrganiserDashboard.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 102 | -------------------------------------------------------------------------------- /frontend/coalesce/src/pages/VolunteerDetail.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 111 | -------------------------------------------------------------------------------- /frontend/coalesce/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import routes from './routes' 5 | 6 | Vue.use(VueRouter) 7 | 8 | /* 9 | * If not building with SSR mode, you can 10 | * directly export the Router instantiation; 11 | * 12 | * The function below can be async too; either use 13 | * async/await or return a Promise which resolves 14 | * with the Router instance. 15 | */ 16 | 17 | export default function (/* { store, ssrContext } */) { 18 | const Router = new VueRouter({ 19 | scrollBehavior: () => ({ x: 0, y: 0 }), 20 | routes, 21 | 22 | // Leave these as they are and change in quasar.conf.js instead! 23 | // quasar.conf.js -> build -> vueRouterMode 24 | // quasar.conf.js -> build -> publicPath 25 | mode: process.env.VUE_ROUTER_MODE, 26 | base: process.env.VUE_ROUTER_BASE 27 | }) 28 | 29 | return Router 30 | } 31 | -------------------------------------------------------------------------------- /frontend/coalesce/src/router/routes.js: -------------------------------------------------------------------------------- 1 | 2 | const routes = [ 3 | { 4 | path: '/', 5 | component: () => import('layouts/MainLayout.vue'), 6 | children: [ 7 | { path: 'login', component: () => import('pages/LogIn.vue') }, 8 | { path: 'opportunities', component: () => import('pages/OpportunitiesSearch.vue') }, 9 | { path: 'opportunities/create', component: () => import('pages/CreateOpportunity.vue') }, 10 | { path: 'opportunities/:id', component: () => import('pages/OpportunityDetails.vue') }, 11 | { path: 'organisers', component: () => import('pages/OrganiserDashboard.vue') }, 12 | { path: 'organisers/create', component: () => import('pages/CreateOrganiser.vue') }, 13 | { path: 'volunteers/:id', component: () => import('pages/VolunteerDetail.vue') }, 14 | { path: '', component: () => import('pages/Index.vue') } 15 | ] 16 | }, 17 | 18 | // Always leave this as last one, 19 | // but you can also remove it 20 | { 21 | path: '*', 22 | component: () => import('pages/Error404.vue') 23 | } 24 | ] 25 | 26 | export default routes 27 | -------------------------------------------------------------------------------- /frontend/coalesce/src/store/auth/actions.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import jwt_decode from 'jwt-decode' 3 | 4 | export function obtainToken (context, { username, password }) { 5 | const payload = { 6 | username: username, 7 | password: password 8 | } 9 | return new Promise((resolve, reject) => { 10 | this._vm.$axios.post(this.state.auth.endpoints.obtainJWT, payload) 11 | .then((response) => { 12 | this.commit('auth/updateToken', response.data) 13 | resolve(response) 14 | }) 15 | .catch((error) => { 16 | reject(error) 17 | }) 18 | }) 19 | } 20 | 21 | export function refreshToken () { 22 | const payload = { 23 | refresh: this.state.jwt.refresh 24 | } 25 | this.$axios.post(this.state.endpoints.refreshJWT, payload) 26 | .then((response) => { 27 | const newToken = { 28 | refresh: this.state.jwt.refresh, 29 | access: response.data.access 30 | } 31 | this.commit('updateToken', newToken) 32 | }) 33 | } 34 | 35 | export function inspectToken () { 36 | const token = this.state.jwt 37 | if (token) { 38 | const decoded = jwt_decode(token) 39 | const exp = decoded.exp 40 | const orig_iat = decoded.orig_iat 41 | 42 | if (exp - (Date.now() / 1000) < 1800 && (Date.now() / 1000) - orig_iat < 628200) { 43 | this.dispatch('refreshToken') 44 | } else if (exp - (Date.now() / 1000) < 1800) { 45 | // DO NOTHING, DO NOT REFRESH 46 | } else { 47 | // TODO: PROMPT USER TO RE-LOGIN, THIS ELSE CLAUSE COVERS THE CONDITION WHERE A TOKEN IS EXPIRED AS WELL 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/coalesce/src/store/auth/getters.js: -------------------------------------------------------------------------------- 1 | export function someGetter (/* state */) { 2 | } 3 | -------------------------------------------------------------------------------- /frontend/coalesce/src/store/auth/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import * as getters from './getters' 3 | import * as mutations from './mutations' 4 | import * as actions from './actions' 5 | 6 | export default { 7 | namespaced: true, 8 | getters, 9 | mutations, 10 | actions, 11 | state 12 | } 13 | -------------------------------------------------------------------------------- /frontend/coalesce/src/store/auth/mutations.js: -------------------------------------------------------------------------------- 1 | export function updateToken (state, newToken) { 2 | localStorage.setItem('t', JSON.stringify(newToken)) 3 | state.jwt = newToken 4 | } 5 | 6 | export function removeToken (state) { 7 | localStorage.removeItem('t') 8 | state.jwt = null 9 | } 10 | -------------------------------------------------------------------------------- /frontend/coalesce/src/store/auth/state.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | return { 3 | // 4 | jwt: JSON.parse(localStorage.getItem('t') || '{}'), 5 | endpoints: { 6 | obtainJWT: '/api/token/', 7 | refreshJWT: '/api/token/refresh/' 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/coalesce/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import auth from './auth' 5 | // import example from './module-example' 6 | 7 | Vue.use(Vuex) 8 | 9 | /* 10 | * If not building with SSR mode, you can 11 | * directly export the Store instantiation; 12 | * 13 | * The function below can be async too; either use 14 | * async/await or return a Promise which resolves 15 | * with the Store instance. 16 | */ 17 | 18 | export default function (/* { ssrContext } */) { 19 | const Store = new Vuex.Store({ 20 | modules: { 21 | // example 22 | auth 23 | }, 24 | 25 | // enable strict mode (adds overhead!) 26 | // for dev mode only 27 | strict: process.env.DEV 28 | }) 29 | 30 | return Store 31 | } 32 | -------------------------------------------------------------------------------- /frontend/coalesce/src/store/store-flag.d.ts: -------------------------------------------------------------------------------- 1 | // THIS FEATURE-FLAG FILE IS AUTOGENERATED, 2 | // REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING 3 | import "quasar/dist/types/feature-flag"; 4 | 5 | declare module "quasar/dist/types/feature-flag" { 6 | interface QuasarFeatureFlags { 7 | store: true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/coalesce/test/jest/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | // Removes 'no-undef' lint errors for Jest global functions (`describe`, `it`, etc), 4 | // add Jest-specific lint rules and Jest plugin 5 | // See https://github.com/jest-community/eslint-plugin-jest#recommended 6 | 'plugin:jest/recommended', 7 | // Uncomment following line to apply style rules 8 | // 'plugin:jest/style', 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /frontend/coalesce/test/jest/__tests__/App.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, createLocalVue, shallowMount } from '@vue/test-utils'; 2 | import QBUTTON from './demo/QBtn-demo.vue'; 3 | import * as All from 'quasar'; 4 | // import langEn from 'quasar/lang/en-us' // change to any language you wish! => this breaks wallaby :( 5 | const { Quasar } = All; 6 | 7 | const components = Object.keys(All).reduce((object, key) => { 8 | const val = All[key]; 9 | if (val && val.component && val.component.name != null) { 10 | object[key] = val; 11 | } 12 | return object; 13 | }, {}); 14 | 15 | describe('Mount Quasar', () => { 16 | const localVue = createLocalVue(); 17 | localVue.use(Quasar, { components }); // , lang: langEn 18 | 19 | const wrapper = mount(QBUTTON, { 20 | localVue, 21 | }); 22 | const vm = wrapper.vm; 23 | 24 | it('has a created hook', () => { 25 | expect(typeof vm.increment).toBe('function'); 26 | }); 27 | 28 | it('accesses the shallowMount', () => { 29 | expect(vm.$el.textContent).toContain('rocket muffin'); 30 | expect(wrapper.text()).toContain('rocket muffin'); // easier 31 | expect(wrapper.find('p').text()).toContain('rocket muffin'); 32 | }); 33 | 34 | it('sets the correct default data', () => { 35 | expect(typeof vm.counter).toBe('number'); 36 | const defaultData2 = QBUTTON.data(); 37 | expect(defaultData2.counter).toBe(0); 38 | }); 39 | 40 | it('correctly updates data when button is pressed', async () => { 41 | const button = wrapper.find('button'); 42 | await button.trigger('click'); 43 | expect(vm.counter).toBe(1); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /frontend/coalesce/test/jest/__tests__/demo/QBtn-demo.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | -------------------------------------------------------------------------------- /frontend/coalesce/test/jest/jest.setup.js: -------------------------------------------------------------------------------- 1 | // No console.log() / setTimeout 2 | // console.log = jest.fn(() => { throw new Error('Do not use console.log() in production') }) 3 | jest.setTimeout(1000) 4 | 5 | global.Promise = require('promise') 6 | 7 | // do this to make sure we don't get multiple hits from both webpacks when running SSR 8 | setTimeout(()=>{ 9 | // do nothing 10 | }, 1) 11 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "coalesce.config") 9 | os.environ.setdefault("DJANGO_CONFIGURATION", "Local") 10 | 11 | try: 12 | from configurations.management import execute_from_command_line 13 | except ImportError: 14 | # The above import may fail for some other reason. Ensure that the 15 | # issue is really that Django is missing to avoid masking other 16 | # exceptions on Python 2. 17 | try: 18 | import django # noqa 19 | except ImportError: 20 | raise ImportError( 21 | "Couldn't import Django. Are you sure it's installed and " 22 | "available on your PYTHONPATH environment variable? Did you " 23 | "forget to activate a virtual environment?" 24 | ) 25 | raise 26 | execute_from_command_line(sys.argv) 27 | 28 | 29 | if __name__ == '__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: coalesce 2 | site_description: An open source volunteer management platform 3 | repo_url: https://github.com/FederationOfTech/coalesce 4 | site_dir: site 5 | copyright: Copyright © 2020, FederationOfTech. 6 | dev_addr: 0.0.0.0:8001 7 | 8 | nav: 9 | - Home: 'index.md' 10 | - API: 11 | - Authentication: 'api/authentication.md' 12 | - Users: 'api/users.md' 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Core 2 | pytz==2020.1 3 | Django==3.0.8 4 | django-configurations==2.2 5 | gunicorn==20.0.4 6 | 7 | # For the persistence stores 8 | psycopg2-binary==2.8.5 9 | dj-database-url==0.5.0 10 | 11 | # Model Tools 12 | django-model-utils==4.0.0 13 | django_unique_upload==0.2.1 14 | 15 | # Rest apis 16 | djangorestframework==3.11.0 17 | Markdown==3.2.2 18 | django-filter==2.3.0 19 | djangorestframework-simplejwt==4.6.0 20 | 21 | # Developer Tools 22 | ipdb==0.13.3 23 | ipython==7.16.1 24 | mkdocs==1.1.2 25 | flake8==3.8.3 26 | 27 | # Testing 28 | mock==4.0.2 29 | factory-boy==2.12.0 30 | django-nose==1.4.6 31 | nose-progressive==1.5.2 32 | coverage==5.2.1 33 | 34 | # Static and Media Storage 35 | django-storages==1.9.1 36 | boto3==1.14.32 37 | 38 | -------------------------------------------------------------------------------- /run-api-tests.sh: -------------------------------------------------------------------------------- 1 | docker-compose down 2 | docker-compose build api 3 | docker-compose up -d api postgres 4 | docker-compose exec api ./manage.py test 5 | docker-compose down -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E226,E302,E41,E702,E731 3 | max-line-length = 110 4 | exclude = migrations 5 | -------------------------------------------------------------------------------- /start_api.sh: -------------------------------------------------------------------------------- 1 | docker-compose down 2 | docker-compose build api 3 | docker-compose up postgres api -------------------------------------------------------------------------------- /wait_for_postgres.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from time import time, sleep 4 | import psycopg2 5 | import dj_database_url 6 | 7 | check_timeout = os.getenv("POSTGRES_CHECK_TIMEOUT", 30) 8 | check_interval = os.getenv("POSTGRES_CHECK_INTERVAL", 1) 9 | interval_unit = "second" if check_interval == 1 else "seconds" 10 | 11 | # Get the database connection details from DATBASE_URL 12 | django_db_config = dj_database_url.config() 13 | 14 | config = { 15 | "dbname": django_db_config.get("NAME", "postgres"), 16 | "user": django_db_config.get("USER", "postgres"), 17 | "password": django_db_config.get("PASSWORD", "postgres"), 18 | "host": django_db_config.get("HOST", "postgres") 19 | } 20 | 21 | start_time = time() 22 | logger = logging.getLogger() 23 | logger.setLevel(logging.INFO) 24 | logger.addHandler(logging.StreamHandler()) 25 | 26 | 27 | def pg_isready(host, user, password, dbname): 28 | while time() - start_time < check_timeout: 29 | try: 30 | conn = psycopg2.connect(**vars()) 31 | logger.info("Postgres is ready! ✨ 💅") 32 | conn.close() 33 | return True 34 | except psycopg2.OperationalError: 35 | logger.info(f"Postgres isn't ready. Waiting for {check_interval} {interval_unit}...") 36 | sleep(check_interval) 37 | 38 | logger.error(f"We could not connect to Postgres within {check_timeout} seconds.") 39 | return False 40 | 41 | 42 | pg_isready(**config) 43 | --------------------------------------------------------------------------------