├── .env.sample ├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── _vimrc_local.vim ├── announcements ├── __init__.py ├── admin.py ├── apps.py ├── feeds.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tests │ ├── __init__.py │ ├── factories.py │ ├── test_feeds.py │ ├── test_models.py │ └── test_views.py ├── urls.py └── views.py ├── core ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200619_2303.py │ ├── 0003_auto_20200623_0109.py │ ├── 0004_auto_20200624_0204.py │ ├── 0005_initial_categories.py │ ├── 0006_service_organization_name.py │ ├── 0007_orderable_services.py │ └── __init__.py ├── models.py ├── tests │ ├── __init__.py │ ├── factories.py │ ├── test_models.py │ └── test_views.py ├── urls.py └── views.py ├── manage.py ├── project ├── __init__.py ├── asgi.py ├── settings.py ├── testing_settings.py ├── urls.py └── wsgi.py ├── pytest.ini ├── requirements-dev.txt ├── requirements.in ├── requirements.txt ├── runtime.txt ├── setup.cfg ├── static ├── categories │ ├── activities-experiences.png │ ├── basic.png │ ├── education-jobs.png │ ├── health.png │ ├── money-legal.png │ └── shelter.png ├── crisis-211.png ├── crisis.png ├── frederick-county.png ├── icon-512x512.png ├── icon.png ├── logos │ └── line-logo.png ├── manifest.json └── ship-logo.jpg └── templates ├── announcements └── announcement_list.html ├── base.html └── core ├── index.html └── servicecategory_detail.html /.env.sample: -------------------------------------------------------------------------------- 1 | DEBUG=on 2 | DEBUG_TOOLBAR=on 3 | GUNICORN_CMD_ARGS="--reload" 4 | PYTHONUNBUFFERED=1 5 | SECRET_KEY=8mp*+j=w@q#w)1251qu48&-srn&rvl2&q0#1i__5^xf+utruyq 6 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Vim 132 | *.sw* 133 | 134 | # Mac 135 | .DS_Store 136 | 137 | # Django 138 | staticfiles/ 139 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tech Frederick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: python manage.py migrate 2 | web: gunicorn project.wsgi --log-file - 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SHIP Haven 2 | 3 | Services web app for SHIP, Student Homelessness Initiative Partnership of Frederick County 4 | 5 | ## Deployment 6 | 7 | These instruction assume that you are already a collaborator 8 | on SHIP's Heroku account. 9 | 10 | The SHIP Haven web application is deployed to Heroku. 11 | Follow the [Heroku documentation](https://devcenter.heroku.com/articles/git#creating-a-heroku-remote) 12 | to add a Heroku remote 13 | to your local repository clone. 14 | 15 | To trigger a deploy, run: 16 | 17 | ```bash 18 | $ git push heroku master 19 | ``` 20 | 21 | This will build a new Heroku slug, 22 | run the `release` command 23 | in the `Procfile` 24 | to run any Django migrations, 25 | and make the new version of the application live. 26 | 27 | ## Developer Setup 28 | 29 | These commands are examples that can run 30 | on a Mac terminal. 31 | Most of the commands should be accurate 32 | for Windows. 33 | Differences are highlighted. 34 | 35 | ### Prerequisites 36 | 37 | Ensure that a version of Python 3 is installed 38 | on your machine. 39 | You can check by running, 40 | 41 | ```bash 42 | $ python3 -V 43 | ``` 44 | 45 | That command should print the version number 46 | (note the capital V character!). 47 | 48 | Deployment happens on Heroku. 49 | You'll want the Heroku CLI installed. 50 | See [The Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) documentation 51 | for how to install the tools. 52 | 53 | ### Actions 54 | 55 | Start a virtual environment. 56 | The `source` command is specific to bash (i.e., Mac or Linux). 57 | To start the virtual environment 58 | on Windows, 59 | check the `venv` documentation 60 | in the [Python documentation](https://docs.python.org/3/library/venv.html). 61 | 62 | ```bash 63 | $ python3 -m venv venv 64 | $ source venv/bin/activate 65 | ``` 66 | 67 | Install developer packages. 68 | 69 | ```bash 70 | $ pip install -r requirements-dev.txt 71 | ``` 72 | 73 | Install application packages. 74 | 75 | ```bash 76 | $ pip install -r requirements.txt 77 | ``` 78 | 79 | The application needs to read environment variables 80 | to run properly 81 | (because that's the Heroku model 82 | of providing configuration via environment variables). 83 | To set those variables locally, 84 | copy the `.env.sample` file 85 | as `.env`. 86 | 87 | ```bash 88 | $ cp .env.sample .env 89 | ``` 90 | 91 | Apply Django database migrations locally. 92 | 93 | ```bash 94 | $ ./manage.py migrate 95 | ``` 96 | 97 | Create a local superuser account. 98 | 99 | ```bash 100 | $ ./manage.py createsuperuser 101 | ``` 102 | 103 | Start the local webserver. 104 | 105 | ```bash 106 | heroku local 107 | ``` 108 | 109 | You can stop the webserver 110 | by pressing `Control+C`. 111 | -------------------------------------------------------------------------------- /_vimrc_local.vim: -------------------------------------------------------------------------------- 1 | let test#python#runner = 'pytest' 2 | -------------------------------------------------------------------------------- /announcements/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/announcements/__init__.py -------------------------------------------------------------------------------- /announcements/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Announcement 4 | 5 | 6 | @admin.register(Announcement) 7 | class AnnouncementModelAdmin(admin.ModelAdmin): 8 | list_display = ("title", "expires_at") 9 | date_hierarchy = "expires_at" 10 | -------------------------------------------------------------------------------- /announcements/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AnnouncementsConfig(AppConfig): 5 | name = 'announcements' 6 | -------------------------------------------------------------------------------- /announcements/feeds.py: -------------------------------------------------------------------------------- 1 | from django.contrib.syndication.views import Feed 2 | from django.urls import reverse 3 | 4 | from .models import Announcement 5 | 6 | 7 | class AnnouncementFeed(Feed): 8 | """Generate an RSS feed that Mailchimp (or any feed reader) can import.""" 9 | 10 | title = "Announcement from SHIP" 11 | description = "Announcements for homeless in Frederick County, Maryland" 12 | 13 | @property 14 | def link(self): 15 | return reverse("announcements:list") 16 | 17 | def items(self): 18 | return Announcement.objects.all()[:5] 19 | 20 | def item_title(self, item): 21 | return item.title 22 | 23 | def item_description(self, item): 24 | return item.description 25 | 26 | def item_link(self, item): 27 | return reverse("announcements:list") + f"#announcement-{item.id}" 28 | -------------------------------------------------------------------------------- /announcements/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-18 01:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Announcement", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("title", models.CharField(max_length=128)), 26 | ("description", models.TextField()), 27 | ("expires_at", models.DateTimeField(db_index=True)), 28 | ], 29 | options={"ordering": ["-expires_at"],}, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /announcements/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/announcements/migrations/__init__.py -------------------------------------------------------------------------------- /announcements/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Announcement(models.Model): 5 | """An announcement to broadcast to the community.""" 6 | 7 | title = models.CharField(max_length=128) 8 | description = models.TextField() 9 | expires_at = models.DateTimeField(db_index=True) 10 | 11 | class Meta: 12 | ordering = ["-expires_at"] 13 | 14 | def __str__(self): 15 | return self.title 16 | -------------------------------------------------------------------------------- /announcements/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/announcements/tests/__init__.py -------------------------------------------------------------------------------- /announcements/tests/factories.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import factory 4 | from django.utils import timezone 5 | 6 | 7 | class AnnouncementFactory(factory.django.DjangoModelFactory): 8 | class Meta: 9 | model = "announcements.Announcement" 10 | 11 | title = factory.Faker("sentence") 12 | description = factory.Faker("paragraph") 13 | expires_at = factory.LazyFunction( 14 | lambda: timezone.now() + datetime.timedelta(days=14) 15 | ) 16 | -------------------------------------------------------------------------------- /announcements/tests/test_feeds.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.utils import timezone 4 | from test_plus.test import TestCase 5 | 6 | from .factories import AnnouncementFactory 7 | 8 | 9 | class TestAnnouncementFeed(TestCase): 10 | def test_ok(self): 11 | announcement = AnnouncementFactory() 12 | 13 | response = self.get("announcements:feed") 14 | 15 | self.assertResponseContains(announcement.title, response, html=False) 16 | self.assertResponseContains(announcement.description, response, html=False) 17 | 18 | def test_limit_items(self): 19 | """A limited number of items is in the feed.""" 20 | AnnouncementFactory( 21 | title="Not going to be there", 22 | expires_at=timezone.now() - datetime.timedelta(days=1), 23 | ) 24 | for i in range(5): 25 | AnnouncementFactory() 26 | 27 | response = self.get("announcements:feed") 28 | 29 | assert "Not going to be there" not in response.content.decode() 30 | -------------------------------------------------------------------------------- /announcements/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.utils import timezone 4 | from test_plus.test import TestCase 5 | 6 | from announcements.models import Announcement 7 | from announcements.tests.factories import AnnouncementFactory 8 | 9 | 10 | class TestAnnouncement(TestCase): 11 | def test_factory(self): 12 | announcement = AnnouncementFactory() 13 | 14 | assert announcement.title 15 | assert announcement.description 16 | assert announcement.expires_at 17 | 18 | def test_str(self): 19 | announcement = AnnouncementFactory() 20 | 21 | assert str(announcement) == announcement.title 22 | 23 | def test_ordering(self): 24 | now = timezone.now() 25 | original_announcement = AnnouncementFactory( 26 | expires_at=now + datetime.timedelta(days=7) 27 | ) 28 | newest_announcement = AnnouncementFactory( 29 | expires_at=now + datetime.timedelta(days=14) 30 | ) 31 | newer_announcement = AnnouncementFactory( 32 | expires_at=now + datetime.timedelta(days=10) 33 | ) 34 | 35 | announcements = Announcement.objects.all() 36 | 37 | assert list(announcements) == [ 38 | newest_announcement, 39 | newer_announcement, 40 | original_announcement, 41 | ] 42 | -------------------------------------------------------------------------------- /announcements/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.utils import timezone 4 | from test_plus.test import TestCase 5 | 6 | from .factories import AnnouncementFactory 7 | 8 | 9 | class TestAnnouncementListView(TestCase): 10 | def test_ok(self): 11 | AnnouncementFactory() 12 | 13 | self.get_check_200("announcements:list") 14 | 15 | def test_excludes_expired_announcements(self): 16 | """Old announcements are not included on the page.""" 17 | AnnouncementFactory(expires_at=timezone.now() - datetime.timedelta(days=1)) 18 | 19 | self.get("announcements:list") 20 | 21 | announcements = self.get_context("announcement_list") 22 | assert list(announcements) == [] 23 | -------------------------------------------------------------------------------- /announcements/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import feeds, views 4 | 5 | app_name = "announcements" 6 | urlpatterns = [ 7 | path("", views.AnnouncementListView.as_view(), name="list"), 8 | path("feed/", feeds.AnnouncementFeed(), name="feed"), 9 | ] 10 | -------------------------------------------------------------------------------- /announcements/views.py: -------------------------------------------------------------------------------- 1 | from django.utils import timezone 2 | from django.views.generic import ListView 3 | 4 | from .models import Announcement 5 | 6 | 7 | class AnnouncementListView(ListView): 8 | def get_queryset(self): 9 | return Announcement.objects.filter(expires_at__gte=timezone.now()) 10 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/core/__init__.py -------------------------------------------------------------------------------- /core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from ordered_model.admin import OrderedModelAdmin 3 | 4 | from .models import Service, ServiceCategory 5 | 6 | 7 | @admin.register(ServiceCategory) 8 | class ServiceCategoryAdmin(OrderedModelAdmin): 9 | list_display = ("name", "move_up_down_links") 10 | prepopulated_fields = {"slug": ("name",)} 11 | 12 | 13 | @admin.register(Service) 14 | class ServiceAdmin(OrderedModelAdmin): 15 | list_display = ("name", "organization_name", "category", "move_up_down_links") 16 | list_filter = ("category",) 17 | ordering = ["category", "order"] 18 | radio_fields = {"category": admin.VERTICAL} 19 | -------------------------------------------------------------------------------- /core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = 'core' 6 | -------------------------------------------------------------------------------- /core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-17 04:06 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="ServiceCategory", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("name", models.CharField(max_length=128)), 27 | ( 28 | "slug", 29 | models.SlugField( 30 | help_text=( 31 | "This is the unique name that will display in the URL." 32 | ) 33 | ), 34 | ), 35 | ], 36 | options={"verbose_name_plural": "service categories",}, 37 | ), 38 | migrations.CreateModel( 39 | name="Service", 40 | fields=[ 41 | ( 42 | "id", 43 | models.AutoField( 44 | auto_created=True, 45 | primary_key=True, 46 | serialize=False, 47 | verbose_name="ID", 48 | ), 49 | ), 50 | ("name", models.CharField(max_length=128)), 51 | ("description", models.TextField()), 52 | ("location", models.CharField(max_length=256)), 53 | ("operating_hours", models.CharField(max_length=256)), 54 | ("phone_number", models.CharField(max_length=32)), 55 | ("email", models.EmailField(max_length=254)), 56 | ( 57 | "category", 58 | models.ForeignKey( 59 | on_delete=django.db.models.deletion.CASCADE, 60 | to="core.ServiceCategory", 61 | ), 62 | ), 63 | ], 64 | ), 65 | ] 66 | -------------------------------------------------------------------------------- /core/migrations/0002_auto_20200619_2303.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-19 23:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField(model_name="service", name="location",), 14 | migrations.AddField( 15 | model_name="service", 16 | name="city", 17 | field=models.CharField(default="", max_length=128), 18 | ), 19 | migrations.AddField( 20 | model_name="service", 21 | name="state", 22 | field=models.CharField(default="", max_length=32), 23 | ), 24 | migrations.AddField( 25 | model_name="service", 26 | name="street_address", 27 | field=models.CharField(default="", max_length=256), 28 | ), 29 | migrations.AddField( 30 | model_name="service", name="website", field=models.URLField(default=""), 31 | ), 32 | migrations.AddField( 33 | model_name="service", 34 | name="zip_code", 35 | field=models.CharField(default="", max_length=16), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /core/migrations/0003_auto_20200623_0109.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-23 01:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0002_auto_20200619_2303"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="service", 15 | name="latitude", 16 | field=models.CharField(blank=True, default="", max_length=16), 17 | ), 18 | migrations.AddField( 19 | model_name="service", 20 | name="longitude", 21 | field=models.CharField(blank=True, default="", max_length=16), 22 | ), 23 | migrations.AlterField( 24 | model_name="service", 25 | name="email", 26 | field=models.EmailField(blank=True, max_length=254), 27 | ), 28 | migrations.AlterField( 29 | model_name="service", 30 | name="operating_hours", 31 | field=models.CharField(blank=True, max_length=256), 32 | ), 33 | migrations.AlterField( 34 | model_name="service", 35 | name="phone_number", 36 | field=models.CharField(blank=True, max_length=32), 37 | ), 38 | migrations.AlterField( 39 | model_name="service", 40 | name="website", 41 | field=models.URLField(blank=True, default=""), 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /core/migrations/0004_auto_20200624_0204.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-24 02:04 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0003_auto_20200623_0109"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="servicecategory", 15 | name="color", 16 | field=models.CharField( 17 | choices=[ 18 | ("red", "Red"), 19 | ("teal", "Teal"), 20 | ("orange", "Orange"), 21 | ("purple", "Purple"), 22 | ("green", "Green"), 23 | ("indigo", "Indigo"), 24 | ("yellow", "Yellow"), 25 | ("gray", "Gray"), 26 | ], 27 | default="gray", 28 | max_length=8, 29 | ), 30 | ), 31 | migrations.AddField( 32 | model_name="servicecategory", 33 | name="description", 34 | field=models.TextField(default=""), 35 | ), 36 | migrations.AddField( 37 | model_name="servicecategory", 38 | name="icon", 39 | field=models.CharField( 40 | default="", 41 | help_text=( 42 | "Upload a 300px x 300px to the 'static' directory. Add file path" 43 | " here." 44 | ), 45 | max_length=128, 46 | ), 47 | ), 48 | ] 49 | -------------------------------------------------------------------------------- /core/migrations/0005_initial_categories.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-24 02:06 2 | 3 | from django.db import migrations 4 | 5 | 6 | def add_categories(apps, schema_editor): 7 | """Create the initial categories.""" 8 | ServiceCategory = apps.get_model("core", "ServiceCategory") 9 | categories = [ 10 | ServiceCategory( 11 | name="Basic Needs", 12 | slug="basic-needs", 13 | description="Food, Clothing, Hygiene, and Transportation", 14 | icon="categories/basic.png", 15 | color="teal", 16 | ), 17 | ServiceCategory( 18 | name="Health", 19 | slug="health", 20 | description="Services, Support, and Counseling", 21 | icon="categories/health.png", 22 | color="orange", 23 | ), 24 | ServiceCategory( 25 | name="Shelter + Centers", 26 | slug="shelter-centers", 27 | description="Temporary Shelters, Host Homes, and Other Housing Needs", 28 | icon="categories/shelter.png", 29 | color="purple", 30 | ), 31 | ServiceCategory( 32 | name="Money + Legal", 33 | slug="money-legal", 34 | description="Advocacy and Counseling", 35 | icon="categories/money-legal.png", 36 | color="green", 37 | ), 38 | ServiceCategory( 39 | name="Education + Jobs", 40 | slug="education-jobs", 41 | description="Services, Support, and Counseling", 42 | icon="categories/education-jobs.png", 43 | color="indigo", 44 | ), 45 | ServiceCategory( 46 | name="Activities + Experiences", 47 | slug="activities-experiences", 48 | description=( 49 | "Funding and Services for Social and Emotional Needs Across a Spectrum" 50 | " of Interests and Activities" 51 | ), 52 | icon="categories/activities-experiences.png", 53 | color="yellow", 54 | ), 55 | ] 56 | ServiceCategory.objects.bulk_create(categories) 57 | 58 | 59 | def remove_categories(apps, schema_editor): 60 | """Remove the activities created by this data migration.""" 61 | ServiceCategory = apps.get_model("core", "ServiceCategory") 62 | ServiceCategory.objects.filter( 63 | slug__in=[ 64 | "basic-needs", 65 | "health", 66 | "shelter-centers", 67 | "money-legal", 68 | "education-jobs", 69 | "activities-experiences", 70 | ] 71 | ).delete() 72 | 73 | 74 | class Migration(migrations.Migration): 75 | 76 | dependencies = [ 77 | ("core", "0004_auto_20200624_0204"), 78 | ] 79 | 80 | operations = [migrations.RunPython(add_categories, remove_categories)] 81 | -------------------------------------------------------------------------------- /core/migrations/0006_service_organization_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-25 01:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0005_initial_categories"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="service", 15 | name="organization_name", 16 | field=models.CharField(default="", max_length=128), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /core/migrations/0007_orderable_services.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-26 23:27 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def fill_in_ordering(apps, schema_editor): 7 | """Give an ordering value to any existing service or category. 8 | 9 | The ordered model doesn't seem to work when the ordered values are the same. 10 | Give an incrementing value to each existing row. 11 | """ 12 | ServiceCategory = apps.get_model("core", "ServiceCategory") 13 | for order, category in enumerate( 14 | ServiceCategory.objects.filter(order=0).order_by("id"), start=1 15 | ): 16 | category.order = order 17 | category.save() 18 | 19 | Service = apps.get_model("core", "Service") 20 | for order, service in enumerate( 21 | Service.objects.filter(order=0).order_by("id"), start=1 22 | ): 23 | service.order = order 24 | service.save() 25 | 26 | 27 | class Migration(migrations.Migration): 28 | 29 | dependencies = [ 30 | ("core", "0006_service_organization_name"), 31 | ] 32 | 33 | operations = [ 34 | migrations.AlterModelOptions(name="service", options={"ordering": ("order",)},), 35 | migrations.AlterModelOptions( 36 | name="servicecategory", 37 | options={ 38 | "ordering": ("order",), 39 | "verbose_name_plural": "service categories", 40 | }, 41 | ), 42 | migrations.AddField( 43 | model_name="service", 44 | name="order", 45 | field=models.PositiveIntegerField( 46 | db_index=True, default=0, editable=False, verbose_name="order" 47 | ), 48 | preserve_default=False, 49 | ), 50 | migrations.AddField( 51 | model_name="servicecategory", 52 | name="order", 53 | field=models.PositiveIntegerField( 54 | db_index=True, default=0, editable=False, verbose_name="order" 55 | ), 56 | preserve_default=False, 57 | ), 58 | migrations.RunPython(fill_in_ordering, migrations.RunPython.noop), 59 | ] 60 | -------------------------------------------------------------------------------- /core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/core/migrations/__init__.py -------------------------------------------------------------------------------- /core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from ordered_model.models import OrderedModel 3 | 4 | 5 | class ServiceCategory(OrderedModel): 6 | """Each service must belong to a category for easy browsing.""" 7 | 8 | name = models.CharField(max_length=128) 9 | slug = models.SlugField( 10 | help_text="This is the unique name that will display in the URL." 11 | ) 12 | description = models.TextField(default="") 13 | icon = models.CharField( 14 | max_length=128, 15 | help_text=( 16 | "Upload a 300px x 300px to the 'static' directory. Add file path here." 17 | ), 18 | default="", 19 | ) 20 | COLOR_CHOICES = ( 21 | ("red", "Red"), 22 | ("teal", "Teal"), 23 | ("orange", "Orange"), 24 | ("purple", "Purple"), 25 | ("green", "Green"), 26 | ("indigo", "Indigo"), 27 | ("yellow", "Yellow"), 28 | ("gray", "Gray"), 29 | ) 30 | color = models.CharField(max_length=8, choices=COLOR_CHOICES, default="gray") 31 | 32 | class Meta(OrderedModel.Meta): 33 | verbose_name_plural = "service categories" 34 | 35 | def __str__(self): 36 | return self.name 37 | 38 | 39 | class Service(OrderedModel): 40 | """A community service that is available to homeless citizens.""" 41 | 42 | name = models.CharField(max_length=128) 43 | organization_name = models.CharField(max_length=128, default="") 44 | description = models.TextField() 45 | website = models.URLField(default="", blank=True) 46 | 47 | street_address = models.CharField(max_length=256, default="") 48 | city = models.CharField(max_length=128, default="") 49 | state = models.CharField(max_length=32, default="") 50 | zip_code = models.CharField(max_length=16, default="") 51 | latitude = models.CharField(max_length=16, default="", blank=True) 52 | longitude = models.CharField(max_length=16, default="", blank=True) 53 | 54 | operating_hours = models.CharField(max_length=256, blank=True) 55 | phone_number = models.CharField(max_length=32, blank=True) 56 | email = models.EmailField(blank=True) 57 | category = models.ForeignKey(ServiceCategory, on_delete=models.CASCADE) 58 | 59 | # Services should be orderable within their category. 60 | order_with_respect_to = "category" 61 | 62 | class Meta(OrderedModel.Meta): 63 | pass 64 | 65 | def __str__(self): 66 | return self.name 67 | -------------------------------------------------------------------------------- /core/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/core/tests/__init__.py -------------------------------------------------------------------------------- /core/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | 4 | class ServiceCategoryFactory(factory.django.DjangoModelFactory): 5 | class Meta: 6 | model = "core.ServiceCategory" 7 | 8 | name = factory.Sequence(lambda n: f"Service Category {n}") 9 | slug = factory.Sequence(lambda n: f"service-category-{n}") 10 | description = factory.Faker("sentence") 11 | icon = "categories/shelter.png" 12 | 13 | 14 | class ServiceFactory(factory.django.DjangoModelFactory): 15 | class Meta: 16 | model = "core.Service" 17 | 18 | name = factory.Sequence(lambda n: f"Service {n}") 19 | organization_name = factory.Faker("company") 20 | description = factory.Faker("paragraph") 21 | website = factory.Faker("url") 22 | street_address = factory.Faker("street_address") 23 | city = factory.Faker("city") 24 | state = factory.Faker("state_abbr") 25 | zip_code = factory.Faker("postcode") 26 | latitude = factory.Faker("latitude") 27 | longitude = factory.Faker("longitude") 28 | operating_hours = "9am - 5pm Monday-Friday" 29 | phone_number = factory.Faker("phone_number") 30 | email = factory.Faker("email") 31 | category = factory.SubFactory(ServiceCategoryFactory) 32 | -------------------------------------------------------------------------------- /core/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from test_plus.test import TestCase 2 | 3 | from core.tests.factories import ServiceFactory, ServiceCategoryFactory 4 | 5 | 6 | class TestServiceCategory(TestCase): 7 | def test_factory(self): 8 | category = ServiceCategoryFactory() 9 | 10 | assert category.name 11 | assert category.slug 12 | assert category.description 13 | assert category.icon 14 | assert category.color 15 | 16 | def test_str(self): 17 | category = ServiceCategoryFactory() 18 | 19 | assert str(category) == category.name 20 | 21 | 22 | class TestService(TestCase): 23 | def test_factory(self): 24 | service = ServiceFactory() 25 | 26 | assert service.name 27 | assert service.organization_name 28 | assert service.description 29 | assert service.website 30 | assert service.street_address 31 | assert service.city 32 | assert service.state 33 | assert service.zip_code 34 | assert service.latitude 35 | assert service.longitude 36 | assert service.operating_hours 37 | assert service.phone_number 38 | assert service.email 39 | assert service.category 40 | 41 | def test_str(self): 42 | service = ServiceFactory() 43 | 44 | assert str(service) == service.name 45 | -------------------------------------------------------------------------------- /core/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.utils import timezone 4 | from test_plus.test import TestCase 5 | 6 | from announcements.tests.factories import AnnouncementFactory 7 | 8 | from .factories import ServiceFactory, ServiceCategoryFactory 9 | 10 | 11 | class TestIndex(TestCase): 12 | def test_ok(self): 13 | self.get_check_200("core:index") 14 | 15 | def test_has_categories(self): 16 | """The service categories are rendered on the page.""" 17 | category = ServiceCategoryFactory() 18 | 19 | response = self.get("core:index") 20 | 21 | self.assertContains(response, category.name) 22 | 23 | def test_has_announcements(self): 24 | announcement = AnnouncementFactory() 25 | 26 | self.get("core:index") 27 | 28 | announcements = self.get_context("announcements") 29 | assert list(announcements) == [announcement] 30 | 31 | def test_excludes_expired_announcements(self): 32 | """Old announcements are not included on the page.""" 33 | AnnouncementFactory(expires_at=timezone.now() - datetime.timedelta(days=1)) 34 | 35 | self.get("core:index") 36 | 37 | announcements = self.get_context("announcements") 38 | assert list(announcements) == [] 39 | 40 | 41 | class TestServiceCategoryDetailView(TestCase): 42 | def test_ok(self): 43 | category = ServiceCategoryFactory() 44 | 45 | self.get_check_200("core:service-category-detail", slug=category.slug) 46 | 47 | def test_has_services(self): 48 | """The service category page lists all services.""" 49 | category = ServiceCategoryFactory() 50 | service = ServiceFactory(category=category) 51 | ServiceFactory() 52 | 53 | self.get("core:service-category-detail", slug=category.slug) 54 | 55 | services = self.get_context("services") 56 | assert list(services) == [service] 57 | -------------------------------------------------------------------------------- /core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = "core" 6 | urlpatterns = [ 7 | path("", views.IndexView.as_view(), name="index"), 8 | path( 9 | "category//", 10 | views.ServiceCategoryDetailView.as_view(), 11 | name="service-category-detail", 12 | ), 13 | ] 14 | -------------------------------------------------------------------------------- /core/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import DetailView, ListView 2 | from django.utils import timezone 3 | 4 | from announcements.models import Announcement 5 | 6 | from .models import Service, ServiceCategory 7 | 8 | 9 | class IndexView(ListView): 10 | queryset = ServiceCategory.objects.all() 11 | template_name = "core/index.html" 12 | 13 | def get_context_data(self, **kwargs): 14 | context = super().get_context_data(**kwargs) 15 | context["announcements"] = Announcement.objects.filter( 16 | expires_at__gte=timezone.now() 17 | ) 18 | return context 19 | 20 | 21 | class ServiceCategoryDetailView(DetailView): 22 | model = ServiceCategory 23 | 24 | def get_context_data(self, **kwargs): 25 | context = super().get_context_data(**kwargs) 26 | context["services"] = Service.objects.filter(category=self.object) 27 | return context 28 | -------------------------------------------------------------------------------- /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', 'project.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/project/__init__.py -------------------------------------------------------------------------------- /project/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for project project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for project project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | import django_heroku 16 | import environ 17 | 18 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 19 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 20 | 21 | env = environ.Env(DEBUG=(bool, False), DEBUG_TOOLBAR=(bool, False)) 22 | env_file = os.path.join(BASE_DIR, ".env") 23 | if os.path.exists(env_file): 24 | environ.Env.read_env(env_file) 25 | 26 | # Quick-start development settings - unsuitable for production 27 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 28 | 29 | SECRET_KEY = env("SECRET_KEY") 30 | 31 | DEBUG = env("DEBUG") 32 | # Control whether the Django Debug Toolbar is enabled or disabled. 33 | DEBUG_TOOLBAR = env("DEBUG_TOOLBAR") 34 | 35 | 36 | ALLOWED_HOSTS = ["*"] 37 | 38 | 39 | # Application definition 40 | 41 | INSTALLED_APPS = [ 42 | "django.contrib.admin", 43 | "django.contrib.auth", 44 | "django.contrib.contenttypes", 45 | "django.contrib.sessions", 46 | "django.contrib.messages", 47 | "django.contrib.staticfiles", 48 | # Third party applications 49 | "ordered_model", 50 | # SHIP applications 51 | "announcements", 52 | "core", 53 | ] 54 | 55 | MIDDLEWARE = [ 56 | "django.middleware.security.SecurityMiddleware", 57 | "django.contrib.sessions.middleware.SessionMiddleware", 58 | "django.middleware.common.CommonMiddleware", 59 | "django.middleware.csrf.CsrfViewMiddleware", 60 | "django.contrib.auth.middleware.AuthenticationMiddleware", 61 | "django.contrib.messages.middleware.MessageMiddleware", 62 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 63 | ] 64 | 65 | if DEBUG and DEBUG_TOOLBAR: 66 | # Enable the debug toolbar only in DEBUG mode. 67 | INSTALLED_APPS.append("debug_toolbar") 68 | MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") 69 | INTERNAL_IPS = ["127.0.0.1"] 70 | 71 | ROOT_URLCONF = "project.urls" 72 | 73 | TEMPLATES = [ 74 | { 75 | "BACKEND": "django.template.backends.django.DjangoTemplates", 76 | "DIRS": [os.path.join(BASE_DIR, "templates")], 77 | "APP_DIRS": True, 78 | "OPTIONS": { 79 | "context_processors": [ 80 | "django.template.context_processors.debug", 81 | "django.template.context_processors.request", 82 | "django.contrib.auth.context_processors.auth", 83 | "django.contrib.messages.context_processors.messages", 84 | ], 85 | }, 86 | }, 87 | ] 88 | 89 | WSGI_APPLICATION = "project.wsgi.application" 90 | 91 | 92 | # Database 93 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 94 | 95 | DATABASES = { 96 | "default": { 97 | "ENGINE": "django.db.backends.sqlite3", 98 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 99 | } 100 | } 101 | 102 | 103 | # Password validation 104 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 105 | 106 | AUTH_PASSWORD_VALIDATORS = [ 107 | { 108 | "NAME": ( 109 | "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 110 | ), 111 | }, 112 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 113 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 114 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 115 | ] 116 | 117 | 118 | # Internationalization 119 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 120 | 121 | LANGUAGE_CODE = "en-us" 122 | 123 | TIME_ZONE = "UTC" 124 | 125 | USE_I18N = True 126 | 127 | USE_L10N = True 128 | 129 | USE_TZ = True 130 | 131 | 132 | # Static files (CSS, JavaScript, Images) 133 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 134 | 135 | STATIC_URL = "/static/" 136 | STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] 137 | 138 | LOGGING = { 139 | "version": 1, 140 | "disable_existing_loggers": False, 141 | "handlers": {"console": {"class": "logging.StreamHandler"}}, 142 | "root": {"handlers": ["console"], "level": "WARNING"}, 143 | } 144 | 145 | django_heroku.settings(locals(), secret_key=False, test_runner=False, logging=False) 146 | -------------------------------------------------------------------------------- /project/testing_settings.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.hashers import BasePasswordHasher 2 | 3 | from .settings import * # noqa 4 | 5 | # An in-memory database should be good enough for now. 6 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} 7 | 8 | 9 | # Migrations are slow to run for tests. Disable them. 10 | class DisableMigrations(object): 11 | def __contains__(self, item): 12 | return True 13 | 14 | def __getitem__(self, item): 15 | return None 16 | 17 | 18 | MIGRATION_MODULES = DisableMigrations() 19 | 20 | 21 | # The password hasher is deliberately slow on the real site. Use a dumb and fast one. 22 | class SimplePasswordHasher(BasePasswordHasher): 23 | """A simple hasher inspired by django-plainpasswordhasher""" 24 | 25 | algorithm = "dumb" # This attribute is needed by the base class. 26 | 27 | def salt(self): 28 | return "" 29 | 30 | def encode(self, password, salt): 31 | return "dumb$$%s" % password 32 | 33 | def verify(self, password, encoded): 34 | algorithm, hash = encoded.split("$$", 1) 35 | assert algorithm == "dumb" 36 | return password == hash 37 | 38 | def safe_summary(self, encoded): 39 | """This is a decidedly unsafe version. The password is returned in the clear.""" 40 | return {"algorithm": "dumb", "hash": encoded.split("$", 2)[2]} 41 | 42 | 43 | PASSWORD_HASHERS = ("project.testing_settings.SimplePasswordHasher",) 44 | 45 | STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" 46 | -------------------------------------------------------------------------------- /project/urls.py: -------------------------------------------------------------------------------- 1 | """project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.contrib import admin 18 | from django.urls import include, path 19 | 20 | # Tweak the admin site. 21 | admin.site.site_header = "SHIP Haven administration" 22 | admin.site.site_title = "SHIP Haven admin" 23 | 24 | urlpatterns = [ 25 | path("", include("core.urls")), 26 | path("announcements/", include("announcements.urls")), 27 | path("shipyard/", admin.site.urls), 28 | ] 29 | 30 | # Enable the debug toolbar only in DEBUG mode. 31 | if settings.DEBUG and settings.DEBUG_TOOLBAR: 32 | import debug_toolbar 33 | 34 | urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns 35 | -------------------------------------------------------------------------------- /project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = project.testing_settings 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | django-test-plus==1.4.0 2 | factory-boy==2.12.0 3 | flake8==3.8.3 4 | pip-tools 5 | pytest-django==3.9.0 6 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | Django==3.1.13 2 | django-debug-toolbar==2.2.1 3 | django-environ==0.4.5 4 | django-heroku==0.3.1 5 | django-ordered-model==3.4.1 6 | gunicorn==20.0.4 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file=requirements.txt requirements.in 6 | # 7 | asgiref==3.3.1 8 | # via django 9 | dj-database-url==0.5.0 10 | # via django-heroku 11 | django==3.1.13 12 | # via 13 | # -r requirements.in 14 | # django-debug-toolbar 15 | # django-heroku 16 | django-debug-toolbar==2.2.1 17 | # via -r requirements.in 18 | django-environ==0.4.5 19 | # via -r requirements.in 20 | django-heroku==0.3.1 21 | # via -r requirements.in 22 | django-ordered-model==3.4.1 23 | # via -r requirements.in 24 | gunicorn==20.0.4 25 | # via -r requirements.in 26 | psycopg2==2.8.5 27 | # via django-heroku 28 | pytz==2020.1 29 | # via django 30 | sqlparse==0.3.1 31 | # via 32 | # django 33 | # django-debug-toolbar 34 | whitenoise==5.1.0 35 | # via django-heroku 36 | 37 | # The following packages are considered to be unsafe in a requirements file: 38 | # setuptools 39 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.7.12 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | ignore = E203, W503 4 | -------------------------------------------------------------------------------- /static/categories/activities-experiences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/static/categories/activities-experiences.png -------------------------------------------------------------------------------- /static/categories/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/static/categories/basic.png -------------------------------------------------------------------------------- /static/categories/education-jobs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/static/categories/education-jobs.png -------------------------------------------------------------------------------- /static/categories/health.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/static/categories/health.png -------------------------------------------------------------------------------- /static/categories/money-legal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/static/categories/money-legal.png -------------------------------------------------------------------------------- /static/categories/shelter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/static/categories/shelter.png -------------------------------------------------------------------------------- /static/crisis-211.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/static/crisis-211.png -------------------------------------------------------------------------------- /static/crisis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/static/crisis.png -------------------------------------------------------------------------------- /static/frederick-county.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/static/frederick-county.png -------------------------------------------------------------------------------- /static/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/static/icon-512x512.png -------------------------------------------------------------------------------- /static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/static/icon.png -------------------------------------------------------------------------------- /static/logos/line-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/static/logos/line-logo.png -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#0f75bd", 3 | "description": "SHIP Haven lists urgent services and critical support in Frederick County", 4 | "display": "fullscreen", 5 | "icons": [ 6 | { 7 | "src": "icon.png", 8 | "sizes": "192x192", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "icon-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png" 15 | } 16 | ], 17 | "name": "Frederick County urgent services and critical support", 18 | "short_name": "SHIP Haven", 19 | "start_url": "/" 20 | } 21 | -------------------------------------------------------------------------------- /static/ship-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TechFrederick/ship/5af0af05e3f949a529b9eaa84e7a9a24ec410985/static/ship-logo.jpg -------------------------------------------------------------------------------- /templates/announcements/announcement_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block main %} 4 |
5 |

Current Announcements

6 |
7 |
8 | {% if announcement_list %} 9 | {% for announcement in announcement_list %} 10 |
11 |

Announcement

12 |

{{ announcement.title }}

13 |

{{ announcement.description }}

14 |
15 | {# TODO: If we can get back callout and URL, then great. #} 16 | {# {% if announcement.callout %} #} 17 | {# #} 27 | {# {% endif %} #} 28 | {% endfor %} 29 | {% endif %} 30 |
31 |
32 |
33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | SHIP Haven 10 | 27 | 28 | 35 | 36 | 37 | 38 |
39 |
40 | 41 | SHIP Haven logo 42 | 43 |
44 | 45 | Call for help 46 | 47 |
48 |
49 | 50 | {% block main %}{% endblock %} 51 |
52 | 53 |
54 |
55 |
56 | Frederick County icon 57 | 58 |
Sign up to receive announcements.
59 |
60 |
61 | 64 | 65 | 66 | 67 |
68 |
69 |
70 |
71 | 72 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /templates/core/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block main %} 5 | 6 |
7 | 8 |
9 | emergency operator icon 10 | 11 |
12 |

Do you need immediate help or just have a question?

13 |

Maryland’s Helpline is available 24/7 to provide support, guidance, and assistance. 14 | Please call 211 and select option 1, text your zip code to 898-211, or visit 211MD.org

15 |
16 |
17 |
18 |
19 | 20 |
21 |
22 |

23 | Services and Support 24 |

25 |
26 | 27 | {% for category in servicecategory_list %} 28 | 39 | {% endfor %} 40 | 41 |
42 | 43 | {% if announcements %} 44 |
45 |
46 |
47 | {% for announcement in announcements %} 48 |
49 |

Announcement

50 |

{{ announcement.title }}

51 |

52 | {{ announcement.description|truncatewords:25 }} 53 | 55 | Continue Reading 56 |

57 |
58 | {% endfor %} 59 |
60 |
61 |
62 | {% endif %} 63 | 64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /templates/core/servicecategory_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block main %} 5 | 16 | 17 |
18 |
19 |
20 | 21 |

{{ servicecategory.name }}

22 |

{{ servicecategory.description }}

23 |
24 |
25 |
26 | 27 |
28 | {% for service in services %} 29 |
30 |
31 |
32 | 33 |
34 |

{{ service.organization_name }}

35 |

{{ service.name }}

36 |

{{ service.description }}

37 |
38 | 39 |

40 | More Information on {{ service.name }} 41 |

42 | 43 |
44 | 45 | {% if service.street_address %} 46 |
47 | {% if service.latitude and service.longitude %} 48 |
49 | 50 | 51 | 52 | {{ service.street_address }}, {{ service.city }}, {{ service.state }} {{ service.zip_code }} 53 |
54 | {% else %} 55 | 56 | 57 | 58 | 59 | {{ service.street_address }}, {{ service.city }}, {{ service.state }} {{ service.zip_code }} 60 | 61 | {% endif %} 62 |
63 | {% endif %} 64 | 65 | {% if service.operating_hours %} 66 |
67 |
68 | 69 | 70 | 71 | {{ service.operating_hours }} 72 |
73 |
74 | {% endif %} 75 | 76 | {% if service.phone_number %} 77 | 85 | {% endif %} 86 | 87 | {% if service.email %} 88 | 96 | {% endif %} 97 | 98 | {% if service.website %} 99 | 107 | {% endif %} 108 |
109 |
110 |
111 |
112 | {% endfor %} 113 |
114 | 115 | {% endblock %} 116 | --------------------------------------------------------------------------------