├── runtime.txt ├── gardenhub ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── populate_starter_crops.py │ │ └── _crops_list.py ├── migrations │ ├── __init__.py │ ├── 0002_pick_comment.py │ └── 0003_garden_map_image.py ├── templatetags │ ├── __init__.py │ └── gardenhub.py ├── static │ ├── fonts │ │ ├── gardenhub.eot │ │ ├── gardenhub.ttf │ │ ├── gardenhub.woff │ │ ├── license.txt │ │ ├── src │ │ │ ├── garden-icon.svg │ │ │ ├── plot-icon.svg │ │ │ └── order-icon.svg │ │ └── gardenhub.svg │ ├── icons │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-150x150.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── browserconfig.xml │ │ ├── manifest.json │ │ └── safari-pinned-tab.svg │ ├── images │ │ ├── login-screen.png │ │ ├── logo.svg │ │ ├── logo-white.svg │ │ ├── logo-small.svg │ │ └── sunrise.svg │ ├── js │ │ └── main.js │ └── less │ │ ├── components │ │ ├── _list_items.less │ │ ├── _nav.less │ │ ├── _login.less │ │ ├── _icons.less │ │ ├── _tapicons.less │ │ └── _welcome_screen.less │ │ └── style.less ├── apps.py ├── templates │ ├── gardenhub │ │ ├── email_invitation.txt │ │ ├── forgot_password.html │ │ ├── partials │ │ │ ├── messages.html │ │ │ ├── order_status.html │ │ │ └── order_card.html │ │ ├── email_picker_new_order.txt │ │ ├── email_inquirer_new_pick.txt │ │ ├── account_confirm_remove.html │ │ ├── order_confirm_cancel.html │ │ ├── garden_list.html │ │ ├── order_list.html │ │ ├── plot_list.html │ │ ├── account_detail.html │ │ ├── garden_detail.html │ │ ├── _base.html │ │ ├── _manage_base.html │ │ ├── garden_form.html │ │ ├── account_activate.html │ │ ├── pick_form.html │ │ ├── account_settings.html │ │ ├── order_detail.html │ │ ├── plot_form.html │ │ ├── homepage.html │ │ └── order_form.html │ └── registration │ │ ├── password_reset_form.html │ │ ├── password_reset_confirm.html │ │ └── login.html ├── __init__.py ├── utils.py ├── mixins.py ├── test_views.py ├── urls.py ├── forms.py ├── factories.py ├── managers.py ├── admin.py └── test_factories.py ├── settings ├── __init__.py ├── dev.py ├── production.py └── base.py ├── .snyk ├── Procfile ├── crops ├── barley.jpg ├── basil.jpg ├── beets.jpg ├── caper.jpg ├── celery.jpg ├── chives.jpg ├── corn.jpg ├── daisy.jpg ├── dill.jpg ├── fennel.jpg ├── garlic.jpg ├── ginger.jpg ├── gourds.jpg ├── hops.jpg ├── kale.jpg ├── kiwi.jpg ├── leeks.jpg ├── luffa.jpg ├── okra.jpg ├── olives.jpg ├── onion.jpg ├── peach.jpg ├── pears.jpg ├── peas.jpg ├── quinoa.jpg ├── radish.jpg ├── sage.jpg ├── sesame.jpg ├── thyme.jpg ├── tulips.jpg ├── yams.jpg ├── amaranth.jpg ├── apricots.jpg ├── avocado.jpg ├── bananas.jpg ├── bay_leaf.jpg ├── bok_choy.jpg ├── broccoli.jpg ├── cabbage.jpg ├── carrots.jpg ├── cherries.jpg ├── cilantro.jpg ├── cucumber.jpg ├── daylily.jpg ├── eggplant.jpg ├── honeydew.jpg ├── kohlrabi.jpg ├── lentils.jpg ├── lettuce.jpg ├── marigold.jpg ├── mustard.jpg ├── oranges.jpg ├── oregano.jpg ├── parsley.jpg ├── peanuts.jpg ├── potatoes.jpg ├── pumpkin.jpg ├── rosemary.jpg ├── shallots.jpg ├── spinach.jpg ├── tomatoes.jpg ├── truffle.jpg ├── walnuts.jpg ├── zucchini.jpg ├── artichoke.jpg ├── asparagus.jpg ├── blueberries.jpg ├── cauliflower.jpg ├── chamomile.jpg ├── chestnuts.jpg ├── chickpeas.jpg ├── coriander.jpg ├── cranberries.jpg ├── grapefruit.jpg ├── green_beans.jpg ├── hot_peppers.jpg ├── peppermint.jpg ├── pineapple.jpg ├── raspberries.jpg ├── red_apple.jpg ├── red_lettuce.jpg ├── red_onions.jpg ├── red_pepper.jpg ├── scallions.jpg ├── snap_peas.jpg ├── snow_peas.jpg ├── soy_beans.jpg ├── starfruit.jpg ├── sunflower.jpg ├── sweet_corn.jpg ├── thai_basil.jpg ├── tomatillo.jpg ├── watermelon.jpg ├── white_onion.jpg ├── acorn_squash.jpg ├── bean_sprouts.jpg ├── blackberries.jpg ├── elderberries.jpg ├── green_apples.jpg ├── green_grapes.jpg ├── green_pepper.jpg ├── huckleberries.jpg ├── purple_grapes.jpg ├── strawberries.jpg ├── yellow_onion.jpg ├── alfalfa_spouts.jpg ├── brussels_sprouts.jpg ├── butternut_squash.jpg ├── cayenne_peppers.jpg ├── cherry_tomatoes.jpg ├── collard_greens.jpg ├── habanero_pepper.jpg ├── jalapeno_peppers.jpg ├── oyster_mushrooms.jpg ├── sweet_potatoes.jpg ├── yellow_peppers.jpg ├── shitake_mushrooms.jpg └── portobello_mushrooms.jpg ├── .dockerignore ├── gardenhub-promo.png ├── docs ├── _static │ ├── client.png │ ├── db-schema.png │ ├── admin-panel.png │ ├── admin-add-user.png │ ├── admin-button.png │ ├── admin-user-form.png │ ├── invite-user-plot.png │ ├── invite-user-garden.png │ └── css │ │ └── custom.css ├── developers │ ├── views.rst │ ├── index.rst │ ├── models.rst │ ├── orders.rst │ └── deployment.rst ├── users │ ├── index.rst │ ├── manage.rst │ ├── intro.rst │ └── invite.rst ├── Makefile ├── index.rst └── logo.svg ├── .gitignore ├── docker-compose.yml ├── requirements.txt ├── wsgi.py ├── manage.py ├── .travis.yml ├── Dockerfile ├── dev.sh ├── CODE_OF_CONDUCT.md └── README.md /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.1 2 | -------------------------------------------------------------------------------- /gardenhub/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gardenhub/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gardenhub/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gardenhub/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .dev import * 2 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | language-settings: 2 | python: "3" 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn wsgi -b 0.0.0.0:5000 2 | -------------------------------------------------------------------------------- /crops/barley.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/barley.jpg -------------------------------------------------------------------------------- /crops/basil.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/basil.jpg -------------------------------------------------------------------------------- /crops/beets.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/beets.jpg -------------------------------------------------------------------------------- /crops/caper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/caper.jpg -------------------------------------------------------------------------------- /crops/celery.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/celery.jpg -------------------------------------------------------------------------------- /crops/chives.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/chives.jpg -------------------------------------------------------------------------------- /crops/corn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/corn.jpg -------------------------------------------------------------------------------- /crops/daisy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/daisy.jpg -------------------------------------------------------------------------------- /crops/dill.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/dill.jpg -------------------------------------------------------------------------------- /crops/fennel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/fennel.jpg -------------------------------------------------------------------------------- /crops/garlic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/garlic.jpg -------------------------------------------------------------------------------- /crops/ginger.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/ginger.jpg -------------------------------------------------------------------------------- /crops/gourds.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/gourds.jpg -------------------------------------------------------------------------------- /crops/hops.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/hops.jpg -------------------------------------------------------------------------------- /crops/kale.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/kale.jpg -------------------------------------------------------------------------------- /crops/kiwi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/kiwi.jpg -------------------------------------------------------------------------------- /crops/leeks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/leeks.jpg -------------------------------------------------------------------------------- /crops/luffa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/luffa.jpg -------------------------------------------------------------------------------- /crops/okra.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/okra.jpg -------------------------------------------------------------------------------- /crops/olives.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/olives.jpg -------------------------------------------------------------------------------- /crops/onion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/onion.jpg -------------------------------------------------------------------------------- /crops/peach.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/peach.jpg -------------------------------------------------------------------------------- /crops/pears.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/pears.jpg -------------------------------------------------------------------------------- /crops/peas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/peas.jpg -------------------------------------------------------------------------------- /crops/quinoa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/quinoa.jpg -------------------------------------------------------------------------------- /crops/radish.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/radish.jpg -------------------------------------------------------------------------------- /crops/sage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/sage.jpg -------------------------------------------------------------------------------- /crops/sesame.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/sesame.jpg -------------------------------------------------------------------------------- /crops/thyme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/thyme.jpg -------------------------------------------------------------------------------- /crops/tulips.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/tulips.jpg -------------------------------------------------------------------------------- /crops/yams.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/yams.jpg -------------------------------------------------------------------------------- /crops/amaranth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/amaranth.jpg -------------------------------------------------------------------------------- /crops/apricots.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/apricots.jpg -------------------------------------------------------------------------------- /crops/avocado.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/avocado.jpg -------------------------------------------------------------------------------- /crops/bananas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/bananas.jpg -------------------------------------------------------------------------------- /crops/bay_leaf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/bay_leaf.jpg -------------------------------------------------------------------------------- /crops/bok_choy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/bok_choy.jpg -------------------------------------------------------------------------------- /crops/broccoli.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/broccoli.jpg -------------------------------------------------------------------------------- /crops/cabbage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/cabbage.jpg -------------------------------------------------------------------------------- /crops/carrots.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/carrots.jpg -------------------------------------------------------------------------------- /crops/cherries.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/cherries.jpg -------------------------------------------------------------------------------- /crops/cilantro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/cilantro.jpg -------------------------------------------------------------------------------- /crops/cucumber.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/cucumber.jpg -------------------------------------------------------------------------------- /crops/daylily.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/daylily.jpg -------------------------------------------------------------------------------- /crops/eggplant.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/eggplant.jpg -------------------------------------------------------------------------------- /crops/honeydew.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/honeydew.jpg -------------------------------------------------------------------------------- /crops/kohlrabi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/kohlrabi.jpg -------------------------------------------------------------------------------- /crops/lentils.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/lentils.jpg -------------------------------------------------------------------------------- /crops/lettuce.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/lettuce.jpg -------------------------------------------------------------------------------- /crops/marigold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/marigold.jpg -------------------------------------------------------------------------------- /crops/mustard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/mustard.jpg -------------------------------------------------------------------------------- /crops/oranges.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/oranges.jpg -------------------------------------------------------------------------------- /crops/oregano.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/oregano.jpg -------------------------------------------------------------------------------- /crops/parsley.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/parsley.jpg -------------------------------------------------------------------------------- /crops/peanuts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/peanuts.jpg -------------------------------------------------------------------------------- /crops/potatoes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/potatoes.jpg -------------------------------------------------------------------------------- /crops/pumpkin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/pumpkin.jpg -------------------------------------------------------------------------------- /crops/rosemary.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/rosemary.jpg -------------------------------------------------------------------------------- /crops/shallots.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/shallots.jpg -------------------------------------------------------------------------------- /crops/spinach.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/spinach.jpg -------------------------------------------------------------------------------- /crops/tomatoes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/tomatoes.jpg -------------------------------------------------------------------------------- /crops/truffle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/truffle.jpg -------------------------------------------------------------------------------- /crops/walnuts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/walnuts.jpg -------------------------------------------------------------------------------- /crops/zucchini.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/zucchini.jpg -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | __pycache__ 3 | *.pyc 4 | db.sqlite3 5 | /static 6 | /media 7 | /docs 8 | -------------------------------------------------------------------------------- /crops/artichoke.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/artichoke.jpg -------------------------------------------------------------------------------- /crops/asparagus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/asparagus.jpg -------------------------------------------------------------------------------- /crops/blueberries.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/blueberries.jpg -------------------------------------------------------------------------------- /crops/cauliflower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/cauliflower.jpg -------------------------------------------------------------------------------- /crops/chamomile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/chamomile.jpg -------------------------------------------------------------------------------- /crops/chestnuts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/chestnuts.jpg -------------------------------------------------------------------------------- /crops/chickpeas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/chickpeas.jpg -------------------------------------------------------------------------------- /crops/coriander.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/coriander.jpg -------------------------------------------------------------------------------- /crops/cranberries.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/cranberries.jpg -------------------------------------------------------------------------------- /crops/grapefruit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/grapefruit.jpg -------------------------------------------------------------------------------- /crops/green_beans.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/green_beans.jpg -------------------------------------------------------------------------------- /crops/hot_peppers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/hot_peppers.jpg -------------------------------------------------------------------------------- /crops/peppermint.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/peppermint.jpg -------------------------------------------------------------------------------- /crops/pineapple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/pineapple.jpg -------------------------------------------------------------------------------- /crops/raspberries.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/raspberries.jpg -------------------------------------------------------------------------------- /crops/red_apple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/red_apple.jpg -------------------------------------------------------------------------------- /crops/red_lettuce.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/red_lettuce.jpg -------------------------------------------------------------------------------- /crops/red_onions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/red_onions.jpg -------------------------------------------------------------------------------- /crops/red_pepper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/red_pepper.jpg -------------------------------------------------------------------------------- /crops/scallions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/scallions.jpg -------------------------------------------------------------------------------- /crops/snap_peas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/snap_peas.jpg -------------------------------------------------------------------------------- /crops/snow_peas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/snow_peas.jpg -------------------------------------------------------------------------------- /crops/soy_beans.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/soy_beans.jpg -------------------------------------------------------------------------------- /crops/starfruit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/starfruit.jpg -------------------------------------------------------------------------------- /crops/sunflower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/sunflower.jpg -------------------------------------------------------------------------------- /crops/sweet_corn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/sweet_corn.jpg -------------------------------------------------------------------------------- /crops/thai_basil.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/thai_basil.jpg -------------------------------------------------------------------------------- /crops/tomatillo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/tomatillo.jpg -------------------------------------------------------------------------------- /crops/watermelon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/watermelon.jpg -------------------------------------------------------------------------------- /crops/white_onion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/white_onion.jpg -------------------------------------------------------------------------------- /gardenhub-promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/gardenhub-promo.png -------------------------------------------------------------------------------- /crops/acorn_squash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/acorn_squash.jpg -------------------------------------------------------------------------------- /crops/bean_sprouts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/bean_sprouts.jpg -------------------------------------------------------------------------------- /crops/blackberries.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/blackberries.jpg -------------------------------------------------------------------------------- /crops/elderberries.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/elderberries.jpg -------------------------------------------------------------------------------- /crops/green_apples.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/green_apples.jpg -------------------------------------------------------------------------------- /crops/green_grapes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/green_grapes.jpg -------------------------------------------------------------------------------- /crops/green_pepper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/green_pepper.jpg -------------------------------------------------------------------------------- /crops/huckleberries.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/huckleberries.jpg -------------------------------------------------------------------------------- /crops/purple_grapes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/purple_grapes.jpg -------------------------------------------------------------------------------- /crops/strawberries.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/strawberries.jpg -------------------------------------------------------------------------------- /crops/yellow_onion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/yellow_onion.jpg -------------------------------------------------------------------------------- /docs/_static/client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/docs/_static/client.png -------------------------------------------------------------------------------- /crops/alfalfa_spouts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/alfalfa_spouts.jpg -------------------------------------------------------------------------------- /crops/brussels_sprouts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/brussels_sprouts.jpg -------------------------------------------------------------------------------- /crops/butternut_squash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/butternut_squash.jpg -------------------------------------------------------------------------------- /crops/cayenne_peppers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/cayenne_peppers.jpg -------------------------------------------------------------------------------- /crops/cherry_tomatoes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/cherry_tomatoes.jpg -------------------------------------------------------------------------------- /crops/collard_greens.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/collard_greens.jpg -------------------------------------------------------------------------------- /crops/habanero_pepper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/habanero_pepper.jpg -------------------------------------------------------------------------------- /crops/jalapeno_peppers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/jalapeno_peppers.jpg -------------------------------------------------------------------------------- /crops/oyster_mushrooms.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/oyster_mushrooms.jpg -------------------------------------------------------------------------------- /crops/sweet_potatoes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/sweet_potatoes.jpg -------------------------------------------------------------------------------- /crops/yellow_peppers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/yellow_peppers.jpg -------------------------------------------------------------------------------- /docs/_static/db-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/docs/_static/db-schema.png -------------------------------------------------------------------------------- /crops/shitake_mushrooms.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/shitake_mushrooms.jpg -------------------------------------------------------------------------------- /docs/_static/admin-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/docs/_static/admin-panel.png -------------------------------------------------------------------------------- /crops/portobello_mushrooms.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/crops/portobello_mushrooms.jpg -------------------------------------------------------------------------------- /docs/_static/admin-add-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/docs/_static/admin-add-user.png -------------------------------------------------------------------------------- /docs/_static/admin-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/docs/_static/admin-button.png -------------------------------------------------------------------------------- /docs/_static/admin-user-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/docs/_static/admin-user-form.png -------------------------------------------------------------------------------- /docs/_static/invite-user-plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/docs/_static/invite-user-plot.png -------------------------------------------------------------------------------- /docs/_static/invite-user-garden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/docs/_static/invite-user-garden.png -------------------------------------------------------------------------------- /gardenhub/static/fonts/gardenhub.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/gardenhub/static/fonts/gardenhub.eot -------------------------------------------------------------------------------- /gardenhub/static/fonts/gardenhub.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/gardenhub/static/fonts/gardenhub.ttf -------------------------------------------------------------------------------- /gardenhub/static/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/gardenhub/static/icons/favicon.ico -------------------------------------------------------------------------------- /docs/developers/views.rst: -------------------------------------------------------------------------------- 1 | Views 2 | ===== 3 | 4 | .. automodule:: gardenhub.views 5 | :members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /gardenhub/static/fonts/gardenhub.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/gardenhub/static/fonts/gardenhub.woff -------------------------------------------------------------------------------- /gardenhub/static/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/gardenhub/static/icons/favicon-16x16.png -------------------------------------------------------------------------------- /gardenhub/static/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/gardenhub/static/icons/favicon-32x32.png -------------------------------------------------------------------------------- /gardenhub/static/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/gardenhub/static/icons/mstile-150x150.png -------------------------------------------------------------------------------- /gardenhub/static/images/login-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/gardenhub/static/images/login-screen.png -------------------------------------------------------------------------------- /gardenhub/static/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/gardenhub/static/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /gardenhub/static/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/gardenhub/static/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /gardenhub/static/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarvestHub/GardenHub/HEAD/gardenhub/static/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | div.wy-side-nav-search { 2 | background-color: #7b2c87; 3 | } 4 | 5 | .wy-nav-top { 6 | background-color: #7b2c87; 7 | } 8 | -------------------------------------------------------------------------------- /gardenhub/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class GardenhubConfig(AppConfig): 5 | name = 'gardenhub' 6 | verbose_name = 'GardenHub' 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | __pycache__ 3 | *.pyc 4 | /db.sqlite3 5 | /static 6 | /media 7 | /settings/local.py 8 | /db.dump 9 | /docs/_build 10 | .coverage 11 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/email_invitation.txt: -------------------------------------------------------------------------------- 1 | {{ inviter.get_full_name }} invited you to join GardenHub. 2 | 3 | If you're interested, click the link below: 4 | 5 | {{ activate_url }} 6 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/forgot_password.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | 3 | {% block content %} 4 |

Reset Password

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /gardenhub/__init__.py: -------------------------------------------------------------------------------- 1 | from django.utils.version import get_version 2 | 3 | # major.minor.patch.release.number 4 | # release must be one of alpha, beta, rc, or final 5 | VERSION = (1, 1, 0, 'final', 0) 6 | 7 | __version__ = get_version(VERSION) 8 | -------------------------------------------------------------------------------- /gardenhub/static/js/main.js: -------------------------------------------------------------------------------- 1 | $('.top.menu .ui.dropdown') 2 | .dropdown({ 3 | "action": "nothing" 4 | }); 5 | 6 | $('form .ui.dropdown') 7 | .dropdown(); 8 | 9 | $('.ui.settings.dropdown') 10 | .dropdown({ 11 | "action": "nothing" 12 | }); 13 | -------------------------------------------------------------------------------- /docs/developers/index.rst: -------------------------------------------------------------------------------- 1 | Developer Guide 2 | =============== 3 | 4 | This section will explain how to hack on GardenHub. 5 | 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | :caption: Contents: 10 | 11 | models 12 | orders 13 | views 14 | deployment 15 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/partials/messages.html: -------------------------------------------------------------------------------- 1 | {% if messages %} 2 |
3 | {% for message in messages %} 4 | 5 | {{ message }} 6 |
7 | {% endfor %} 8 | 9 | {% endif %} 10 | -------------------------------------------------------------------------------- /docs/users/index.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | ========== 3 | 4 | This is a walkthrough for garden managers and administrators of the app. This guide will show you how to perform common administrative tasks. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: Contents: 9 | 10 | intro 11 | invite 12 | manage 13 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/email_picker_new_order.txt: -------------------------------------------------------------------------------- 1 | Hey {{ picker.first_name }}! A new order has been submitted for plot {{ order.plot.title }} in {{ order.plot.garden.title }}. 2 | 3 | Start date: {{ order.start_date }} 4 | End date: {{ order.end_date }} 5 | 6 | Order ID: {{ order.id }} 7 | Requester: {{ order.requester }} 8 | -------------------------------------------------------------------------------- /gardenhub/static/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #9f00a7 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /gardenhub/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from django.utils import timezone 3 | 4 | 5 | def today(): 6 | return timezone.localtime().replace( 7 | hour=0, minute=0, second=0, microsecond=0) 8 | 9 | 10 | def localdate(date): 11 | return timezone.make_aware( 12 | datetime(date.year, date.month, date.day)) 13 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/email_inquirer_new_pick.txt: -------------------------------------------------------------------------------- 1 | Hi {{ inquirer.first_name }}! {{ pick.picker.first_name }} has picked your crops from plot {{ pick.plot.title }} in {{ pick.plot.garden.title }}. 2 | {% if pick.comment %} 3 | 4 | {{ pick.picker.first_name }} said: {{ pick.comment }} 5 | {% endif %} 6 | 7 | Crops picked: {{ pick.crops.all|join:", " }} 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: postgres:10-alpine 6 | volumes: 7 | - db:/var/lib/postgresql/data/ 8 | web: 9 | build: . 10 | command: python manage.py runserver 0.0.0.0:5000 11 | volumes: 12 | - .:/app 13 | ports: 14 | - "5000:5000" 15 | depends_on: 16 | - db 17 | 18 | volumes: 19 | db: 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django==2.1.2 2 | django-compressor==2.2 3 | Pillow==5.3.0 4 | whitenoise==4.1 5 | sorl-thumbnail==12.5.0 6 | psycopg2==2.7.5 7 | dj-database-url==0.5.0 8 | django-phonenumber-field==2.0.1 9 | django-storages==1.7.1 10 | boto3==1.9.22 11 | 12 | # Tests 13 | factory_boy==2.11.1 14 | 15 | # Docs 16 | Sphinx==1.8.1 17 | sphinx-autobuild==0.7.1 18 | sphinxcontrib-django==0.4 19 | sphinx_rtd_theme==0.4.2 20 | -------------------------------------------------------------------------------- /gardenhub/static/fonts/license.txt: -------------------------------------------------------------------------------- 1 | Plot icon 2 | "Community Garden Plot Icon" by Alex Gleason 3 | CC0 4 | https://openclipart.org/detail/296147/community-garden-plot-icon 5 | 6 | Garden icon 7 | "Fence" by Daniela Baptista, PT 8 | CC-BY 9 | https://thenounproject.com/icon/1179077/ 10 | 11 | Order icon 12 | "Accounting" by Symbolon, IT 13 | CC-BY 14 | https://thenounproject.com/icon/1137184/ 15 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for gardenhub 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/2.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", "settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /gardenhub/static/less/components/_list_items.less: -------------------------------------------------------------------------------- 1 | .member-of-list-item::after { 2 | content: ", "; 3 | margin: 0px; 4 | padding: 0px; 5 | } 6 | 7 | .member-of-list-item:last-of-type::before { 8 | content: "and "; 9 | margin: 0px; 10 | padding: 0px; 11 | } 12 | 13 | .member-of-list-item:last-of-type::after { 14 | content: ""; 15 | } 16 | 17 | .member-of-list-item:only-of-type::after { 18 | content: ""; 19 | } 20 | 21 | .member-of-list-item:only-of-type::before { 22 | content: ""; 23 | } 24 | -------------------------------------------------------------------------------- /gardenhub/static/less/components/_nav.less: -------------------------------------------------------------------------------- 1 | .top.menu { 2 | background-color: #f9f9fa; 3 | } 4 | 5 | body.has-top-menu { 6 | padding-top: 60px; 7 | } 8 | 9 | .skinny.menu > .item { 10 | padding-left: 0.8em; 11 | padding-right: 0.8em; 12 | 13 | > .text { 14 | font-family: 'Open Sans Condensed', sans-serif; 15 | font-size: 1.2rem; 16 | } 17 | 18 | > i.dropdown.icon { 19 | margin-left: 0.5em; 20 | } 21 | 22 | > .ui.avatar.image { 23 | margin-right: 0.733em; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /gardenhub/migrations/0002_pick_comment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.3 on 2018-04-10 19:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('gardenhub', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='pick', 15 | name='comment', 16 | field=models.TextField(blank=True, help_text='Additional comments about this pick.'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/account_confirm_remove.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | 3 | {% block content %} 4 |

Remove Account

5 |

If you remove your account, you will not be able to log in or recover your account. Are you sure you want to do this?

6 |
7 | {% csrf_token %} 8 | 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/order_confirm_cancel.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | 3 | {% block content %} 4 |

Cancel Order #{{ order.id }}

5 |

If you cancel this order, your crops will not be picked. Please allow for 12 hours notice when cancelling an order.

6 |
7 | {% csrf_token %} 8 | 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /gardenhub/static/icons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GardenHub", 3 | "icons": [ 4 | { 5 | "src": "/static/icons/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image/png" 8 | }, 9 | { 10 | "src": "/static/icons/android-chrome-512x512.png", 11 | "sizes": "512x512", 12 | "type": "image/png" 13 | } 14 | ], 15 | "theme_color": "#7b2c87", 16 | "background_color": "#7b2c87", 17 | "display": "standalone" 18 | } -------------------------------------------------------------------------------- /gardenhub/migrations/0003_garden_map_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.3 on 2018-04-30 19:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('gardenhub', '0002_pick_comment'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='garden', 15 | name='map_image', 16 | field=models.ImageField(blank=True, help_text='A map of this garden.', upload_to='maps'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /gardenhub/mixins.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.mixins import UserPassesTestMixin 2 | 3 | 4 | class UserCanEditGardenMixin(UserPassesTestMixin): 5 | """ 6 | Only a user who can edit this Garden may access this. 7 | """ 8 | def test_func(self): 9 | return self.request.user.can_edit_garden(self.get_object()) 10 | 11 | 12 | class UserCanEditPlotMixin(UserPassesTestMixin): 13 | """ 14 | Only a user who can edit this Plot may access this. 15 | """ 16 | def test_func(self): 17 | return self.request.user.can_edit_plot(self.get_object()) 18 | -------------------------------------------------------------------------------- /settings/dev.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | 3 | 4 | # SECURITY WARNING: don't run with debug turned on in production! 5 | DEBUG = True 6 | TEMPLATES[0]['OPTIONS']['debug'] = True # noqa 7 | 8 | # SECURITY WARNING: keep the secret key used in production secret! 9 | SECRET_KEY = 'CHANGEME!!!' 10 | 11 | INTERNAL_IPS = ('127.0.0.1', '10.0.2.2') 12 | ALLOWED_HOSTS = ['*'] 13 | 14 | BASE_URL = 'http://localhost:8000' 15 | 16 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 17 | 18 | 19 | try: 20 | from .local import * # noqa 21 | except ImportError: 22 | pass 23 | -------------------------------------------------------------------------------- /docs/developers/models.rst: -------------------------------------------------------------------------------- 1 | Models 2 | ====== 3 | 4 | .. image:: /_static/db-schema.png 5 | 6 | Querying objects 7 | ---------------- 8 | 9 | .. automodule:: gardenhub.managers 10 | 11 | .. autoclass:: OrderQuerySet 12 | :members: 13 | 14 | The following fields enable filtering orders via ``Order.objects``. **For 15 | example,** 16 | 17 | .. code-block:: python 18 | 19 | from gardenhub.models import Order 20 | 21 | # All open orders 22 | orders = Order.objects.open() 23 | 24 | # All open orders that haven't been picked today 25 | orders = Order.objects.open().unpicked_today() 26 | 27 | .. autoclass:: UserManager 28 | :members: 29 | 30 | 31 | Full model reference 32 | -------------------- 33 | .. automodule:: gardenhub.models 34 | :members: 35 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = GardenHub 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | livehtml: 18 | sphinx-autobuild -b html $(SPHINXOPTS) $(SOURCEDIR) $(BUILDDIR)/html 19 | 20 | # Catch-all target: route all unknown targets to Sphinx using the new 21 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 22 | %: Makefile 23 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 24 | -------------------------------------------------------------------------------- /gardenhub/static/less/components/_login.less: -------------------------------------------------------------------------------- 1 | body.login { 2 | background-color: #7b2c87; 3 | background-image: url("/static/images/login-screen.png"); 4 | 5 | h1.ui.header { 6 | font-family: 'Saira Extra Condensed', sans-serif; 7 | font-weight: bold; 8 | font-size: 68px; 9 | color: white; 10 | 11 | img.logo { 12 | width: 0.65em; 13 | margin: 0; 14 | margin-top: -0.20em; 15 | } 16 | } 17 | 18 | > .grid { 19 | height: 100%; 20 | margin: 0; 21 | } 22 | 23 | .image { 24 | margin-top: -100px; 25 | } 26 | 27 | .column { 28 | max-width: 450px; 29 | } 30 | 31 | a.password-reset { 32 | color: white; 33 | text-decoration: underline; 34 | } 35 | 36 | /* Create new block formatting context */ 37 | &::before { 38 | content: ''; 39 | display: flex; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/partials/order_status.html: -------------------------------------------------------------------------------- 1 | {% if order.canceled %} 2 | Canceled on {{ order.canceled_timestamp }} 3 | {% elif order.is_closed %} 4 | This order is complete. 5 | {% elif order.is_active %} 6 | {% with order.get_picks|length as pick_count %} 7 | {% if pick_count > 0 %} 8 | Your crops have been picked {{ pick_count }} time{{ pick_count|pluralize }}. 9 | {% else %} 10 | {{ order.plot.garden.pickers.first.first_name }} will be picking your crops soon, hang tight! 11 | {% endif %} 12 | {% endwith %} 13 |
14 |
15 |
16 | {% else %} 17 | This order is scheduled to begin on {{ order.start_date }}. 18 | {% endif %} 19 | -------------------------------------------------------------------------------- /gardenhub/static/less/components/_icons.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'gardenhub'; 3 | src: url('../fonts/gardenhub.eot?tqt8fz'); 4 | src: url('../fonts/gardenhub.eot?tqt8fz#iefix') format('embedded-opentype'), 5 | url('../fonts/gardenhub.ttf?tqt8fz') format('truetype'), 6 | url('../fonts/gardenhub.woff?tqt8fz') format('woff'), 7 | url('../fonts/gardenhub.svg?tqt8fz#gardenhub') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | .plot, 13 | .garden, 14 | .order, 15 | .gardenhub { 16 | &.icon { 17 | font-family: 'gardenhub' !important; 18 | } 19 | } 20 | 21 | .plot.icon::before { 22 | content: "\e900"; 23 | } 24 | 25 | .garden.icon::before { 26 | content: "\e901"; 27 | } 28 | 29 | .order.icon::before { 30 | content: "\e902"; 31 | } 32 | 33 | .gardenhub.icon::before { 34 | content: "\e903"; 35 | } 36 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django # noqa 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | before_script: 7 | - docker build -t gardenhub . 8 | - docker run --name db -e POSTGRES_PASSWORD=gardenhub -d postgres:10-alpine && sleep 5 9 | - docker run -it --name gardenhub --link db:postgres -e CI -e TRAVIS -e TRAVIS_JOB_ID -e TRAVIS_PULL_REQUEST -e DATABASE_URL="postgres://postgres:gardenhub@db:5432/postgres" -d gardenhub sh 10 | - docker exec gardenhub pip install coverage coveralls 11 | 12 | script: 13 | - docker exec gardenhub coverage run --source='.' manage.py test gardenhub 14 | 15 | after_success: 16 | - apk add git 17 | - docker exec gardenhub coveralls 18 | 19 | notifications: 20 | webhooks: 21 | urls: 22 | - "https://scalar.vector.im/api/neb/services/hooks/dHJhdmlzLWNpLyU0MGFsZXhnbGVhc29uJTNBbWF0cml4Lm9yZy8lMjFPVXNkbEF3Z09Cc09PR0hwUVYlM0FtYXRyaXgub3Jn" 23 | on_success: change # always|never|change 24 | on_failure: change 25 | on_start: never 26 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. GardenHub documentation master file, created by 2 | sphinx-quickstart on Wed Jan 10 16:14:05 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to GardenHub's documentation! 7 | ===================================== 8 | 9 | GardenHub is an open source web application that facilitates food distribution 10 | for community gardens. It's written in Python_ using the `Django web 11 | framework`_. 12 | 13 | `GardenHub's source code`_ may be found on GitHub. 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | :caption: Contents: 18 | 19 | developers/index 20 | users/index 21 | 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | 30 | .. _Python: https://www.python.org/ 31 | .. _Django web framework: https://www.djangoproject.com/ 32 | .. _GardenHub's source code: https://github.com/HarvestHub/GardenHub 33 | -------------------------------------------------------------------------------- /docs/users/manage.rst: -------------------------------------------------------------------------------- 1 | Managing plots/gardens 2 | ======================== 3 | 4 | Adding yourself to a plot 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | If you want the ability to invite people via email to a plot, you will need to first assign yourself to that plot so you can access the plot within the client view. 8 | 9 | 1. Visit the admin panel. 10 | 2. Click "Plots". 11 | 3. Click the name of the plot you'd like to assign yourself to. 12 | 4. Under the "Gardeners" field, find your name, and while holding the "Control" key (or "Command" key on MacOS), click it. 13 | 5. Click "Save". 14 | 15 | Adding yourself to a garden 16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 17 | 18 | If you want the ability to invite people via email to a garden, you will need to first assign yourself to that garden so you can access the garden within the client view. 19 | 20 | 1. Visit the admin panel. 21 | 2. Click "Gardens". 22 | 3. Click the name of the garden you'd like to assign yourself to. 23 | 4. Under the "Managers" field, find your name, and while holding the "Control" key (or "Command" key on MacOS), click it. 24 | 5. Click "Save". 25 | -------------------------------------------------------------------------------- /gardenhub/templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_base.html" %} 2 | {% load i18n static %} 3 | 4 | {% block body_class %}login{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |
{% csrf_token %} 10 |
11 |

{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}

12 |
13 | 14 |
15 | 16 | 17 |
18 | {{ form.email.errors }} 19 |
20 | 21 |
22 |
23 |
24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/garden_list.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | {% load gardenhub thumbnail %} 3 | 4 | {% block content %} 5 |

Manage Gardens

6 | 7 |
8 | {% for garden in gardens %} 9 |
10 | 11 | {% if garden.photo %} 12 | {% thumbnail garden.photo "300x300" crop="center" as photo %} 13 | 14 | {% endthumbnail %} 15 | {% else %} 16 | 17 | {% endif %} 18 | 19 |
20 | {{ garden.title }} 21 |
22 |

{{ garden.address }}

23 | {% garden_user_orders garden as orders %} 24 |

{{ orders|length }} upcoming orders.

25 |
26 |
27 |
28 | {% endfor %} 29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /gardenhub/static/less/style.less: -------------------------------------------------------------------------------- 1 | @import "components/_login.less"; 2 | @import "components/_nav.less"; 3 | @import "components/_icons.less"; 4 | @import "components/_tapicons.less"; 5 | @import "components/_welcome_screen.less"; 6 | @import "components/_list_items.less"; 7 | 8 | 9 | html { 10 | background: white; 11 | } 12 | 13 | body { 14 | background-position: center; 15 | background-size: cover; 16 | min-height: 100vh; 17 | } 18 | 19 | body > .content { 20 | min-height: 100%; 21 | padding: 40px 0; 22 | } 23 | 24 | .ui.cards { 25 | /* Not sure why Semantic UI does this, cards shouldn't have a negative top/bottom margin */ 26 | margin-top: 0; 27 | margin-bottom: 0; 28 | } 29 | 30 | .ui.form, 31 | .ui.container { 32 | /* Clearfix */ 33 | &::after { 34 | content: ''; 35 | display: table; 36 | clear: both; 37 | } 38 | } 39 | 40 | .card.picked { 41 | opacity: 0.7; 42 | filter: grayscale(1); 43 | } 44 | 45 | /* I hate that this is necessary */ 46 | .clearfix { 47 | &::after { 48 | content: ''; 49 | display: table; 50 | clear: both; 51 | } 52 | } 53 | 54 | .tapicons .add-crops.button { 55 | margin: 27px 0 0 20px; 56 | } 57 | 58 | .clickable { 59 | cursor: pointer; 60 | } 61 | 62 | .errorlist { 63 | color: #9f3a38; 64 | } 65 | 66 | .ui.button:first-child + .ui.header { 67 | margin-top: -.14285714em; 68 | } 69 | 70 | form .ui.input.upload { 71 | display: block; 72 | } 73 | -------------------------------------------------------------------------------- /docs/developers/orders.rst: -------------------------------------------------------------------------------- 1 | Order states 2 | ============ 3 | 4 | Orders are often queried by one of more possible states they can be in. Below is a chart of all the possible states. 5 | 6 | ========== ====== ======= ======== ================ ======== 7 | State: open closed upcoming active inactive 8 | ========== ====== ======= ======== ================ ======== 9 | start date Any Past Future Up through today Future 10 | end date Future Past Future Today forward Past 11 | canceled No Coerced No No Coerced 12 | comparison and and and and or 13 | ========== ====== ======= ======== ================ ======== 14 | 15 | **Legend:** 16 | 17 | * **canceled = No** means that if the order is canceled it *cannot* be this state. 18 | * **canceled = Coerced** means that the order will automatically be in this state by virtue of the fact it is canceled. 19 | * **comparison** describes how the start date and end date of the order are compared. For instance, an open order can have any start date **and** an end date in the future. An inactive order can have a start date in the future **or** an end date in the past. 20 | 21 | QuerySet filtering 22 | ------------------ 23 | 24 | Corresponding to the table above, orders have custom QuerySet functions for each of the states. For instance, ``Order.objects.open()`` or ``Order.objects.active()``. 25 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/order_list.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | 3 | {% block content %} 4 | 5 | 6 | New Order 7 | 8 | 9 |

10 | Your Orders 11 | These orders are currently underway. 12 |

13 | {% if orders.active %} 14 |
15 | {% for order in orders.open %} 16 | {% include "gardenhub/partials/order_card.html" %} 17 | {% endfor %} 18 |
19 | {% else %} 20 |

You don't have any open orders. Would you like to place one?

21 | {% endif %} 22 | 23 |

24 | Order History 25 | Review orders that have passed. 26 |

27 | {% if orders.closed %} 28 |
29 | {% for order in orders.closed %} 30 | {% include "gardenhub/partials/order_card.html" %} 31 | {% endfor %} 32 |
33 | {% else %} 34 |

Your order history is empty. Once an order you placed finishes it will appear here.

35 | {% endif %} 36 | 37 | {% endblock %} 38 | 39 | {% block extra_scripts %} 40 | 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /gardenhub/templatetags/gardenhub.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.simple_tag(takes_context=True) 7 | def garden_user_orders(context, garden): 8 | """ 9 | Return the user's Orders for the given Garden. 10 | """ 11 | user = context.request.user 12 | orders = user.get_orders().filter(plot__garden__id=garden.id) 13 | return orders 14 | 15 | 16 | @register.simple_tag(takes_context=True) 17 | def plot_user_orders(context, plot): 18 | """ 19 | Return the user's Orders for the given Plot. 20 | """ 21 | user = context.request.user 22 | orders = user.get_orders().filter(plot__id=plot.id) 23 | return orders 24 | 25 | 26 | @register.simple_tag 27 | def placeholder(size, obj): 28 | letter = str(obj)[0] 29 | url = "//via.placeholder.com/{}/a333c8/ffffff?text={}" 30 | return url.format(size, letter) 31 | 32 | 33 | @register.filter 34 | def combine(value, queryset): 35 | """ 36 | Unites two querysets and returns distinct values 37 | """ 38 | return value.union(queryset).distinct() 39 | 40 | 41 | @register.filter 42 | def picker_format(value, garden): 43 | """ 44 | Format Orders for the Picker view. 45 | """ 46 | return value.filter(plot__garden__id=garden.id).active() 47 | 48 | 49 | @register.filter 50 | def user_can_edit_order(value, order): 51 | """ 52 | Ensure that the user can edit the given order. 53 | """ 54 | return value.can_edit_order(order) 55 | -------------------------------------------------------------------------------- /docs/users/intro.rst: -------------------------------------------------------------------------------- 1 | Client view vs admin panel 2 | ========================== 3 | 4 | There are two main parts of the site that are used to manage data: the **client view** and the **admin panel**. 5 | 6 | There are some things that can only be accomplished within the client view, and others that can only be accomplished in the admin panel. 7 | 8 | Client view 9 | ----------- 10 | 11 | The client view is what end-users will see. This design is geared towards gardeners and pickers. It is the default view that will show when visiting the GardenHub app. In the client view, **people can only see plots and gardens they are assigned to**, even if they are an administrator. This is by design. Administrators are treated just like any other user in this view with no special capabilities. 12 | 13 | The client view is the only place that users can invite other users to GardenHub via email. 14 | 15 | .. image:: /_static/client.png 16 | 17 | Admin panel 18 | ----------- 19 | 20 | The admin panel is a tool for directly editing data in GardenHub's database. You can use this to manually create users, manage gardens and plots, and edit the master list of crops. Administrators can access the entire site's database through this view, regardless of whether they're assigned to those gardens or plots. 21 | 22 | .. image:: /_static/admin-panel.png 23 | 24 | The admin panel can be accessed by first logging into GardenHub, then going under your user dropdown in the top navigation and clicking "Admin". Note that only superusers and staff members can access the admin panel. 25 | 26 | .. image:: /_static/admin-button.png 27 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/partials/order_card.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
Order #{{ order.id }}
5 |
6 |
7 |

Location

8 |

{{ order.plot.garden }}, Plot {{ order.plot.title }}

9 | 10 | {% if request.user.is_garden_manager %} 11 |

Requester

12 | {{ order.requester.get_full_name }} 13 | {% endif %} 14 | 15 |

Date Range

16 |
17 | {{ order.start_date }}–{{ order.end_date }} 18 |
19 | 20 |

Status

21 | {% include "gardenhub/partials/order_status.html" %} 22 | 23 |

Crops to be Picked

24 | {% if order.pick_all %} 25 |

All crops on the plot

26 | {% elif order.crops %} 27 |
28 | {% for crop in order.crops.all %} 29 |
30 | 31 |
32 |
{{ crop.title }}
33 |
34 |
35 | {% endfor %} 36 |
37 | {% else %} 38 |

No crops were selected for this order.

39 | {% endif %} 40 |
41 | 42 | 43 | View Order 44 | 45 |
46 | -------------------------------------------------------------------------------- /gardenhub/management/commands/populate_starter_crops.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.db.utils import IntegrityError 3 | from django.core.management.base import BaseCommand 4 | from django.conf import settings 5 | from django.core.files import File 6 | from gardenhub.models import Crop 7 | from ._crops_list import crops_list 8 | 9 | 10 | class Command(BaseCommand): 11 | help = 'Populates 100+ sane default crops into the database' 12 | 13 | def handle(self, *args, **options): 14 | # Directory where the crop images exist 15 | CROPS_DIR = os.path.join(settings.BASE_DIR, 'crops') 16 | 17 | # Loop through list of crops and create them 18 | # The list is in _crops_list.py 19 | for crop_title in crops_list: 20 | # This *should* be the crop's filename 21 | filename = '{}.jpg'.format(crop_title.replace(' ', '_')) 22 | # File representing the exact location of the image 23 | crop_image = File(open( 24 | os.path.join(CROPS_DIR, filename), 'rb')) 25 | try: 26 | # Create the crop and copy the image into it 27 | crop = Crop.objects.create(title=crop_title) 28 | crop.image.save(filename, crop_image) 29 | 30 | self.stdout.write(self.style.SUCCESS( 31 | 'Created {}.'.format(crop_title) 32 | )) 33 | except IntegrityError: 34 | # If the crop already exists, it'll raise an IntegrityError 35 | # so just notify that it was skipped 36 | self.stdout.write(self.style.WARNING( 37 | '{} already exists. Skipping.'.format(crop_title) 38 | )) 39 | 40 | self.stdout.write(self.style.SUCCESS('Finished creating crops!')) 41 | -------------------------------------------------------------------------------- /gardenhub/templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_base.html" %} 2 | {% load i18n static %} 3 | 4 | {% block body_class %}login{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |
{% csrf_token %} 10 |
11 | {% if validlink %} 12 |

{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}

13 |
14 | 15 |
16 | 17 | 18 |
19 | {{ form.new_password1.errors }} 20 |
21 |
22 | 23 |
24 | 25 | 26 |
27 | {{ form.new_password2.errors }} 28 |
29 | 30 | {% else %} 31 |

{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}

32 | {% endif %} 33 |
34 |
35 |
36 |
37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /gardenhub/static/less/components/_tapicons.less: -------------------------------------------------------------------------------- 1 | @tapicon-size: 75px; 2 | @tapicon-gutter: 20px; 3 | @tapicon-size-sm: 50px; 4 | 5 | .tapicons { 6 | display: flex; 7 | align-items: flex-start; 8 | flex-wrap: wrap; 9 | margin: -@tapicon-gutter 0 0 -@tapicon-gutter; 10 | } 11 | 12 | .tapicon { 13 | cursor: pointer; 14 | margin: @tapicon-gutter 0 0 @tapicon-gutter; 15 | 16 | .title { 17 | font-family: 'Open Sans Condensed', sans-serif; 18 | font-size: 1rem; 19 | text-align: center; 20 | } 21 | 22 | .image { 23 | width: @tapicon-size; 24 | height: @tapicon-size; 25 | background-color: gray; 26 | border-radius: 3px; 27 | border-width: 2px; 28 | border-style: solid; 29 | border-color: transparent; 30 | position: relative; 31 | display: flex; 32 | align-items: center; 33 | justify-content: center; 34 | transition: border-color 0.2s; 35 | 36 | img { 37 | width: 100%; 38 | height: 100%; 39 | } 40 | 41 | &::before { 42 | content: ''; 43 | position: absolute; 44 | width: 100%; 45 | height: 100%; 46 | top: 0; 47 | left: 0; 48 | right: 0; 49 | bottom: 0; 50 | background: black; 51 | transition: opacity 0.2s; 52 | opacity: 0; 53 | } 54 | 55 | .check.icon { 56 | position: absolute; 57 | width: 100%; 58 | height: 100%; 59 | top: 0; 60 | left: 0; 61 | right: 0; 62 | bottom: 0; 63 | display: flex; 64 | justify-content: center; 65 | align-items: center; 66 | transition: opacity 0.2s; 67 | opacity: 0; 68 | } 69 | } 70 | 71 | &.active { 72 | 73 | .image { 74 | border-color: limegreen; 75 | 76 | &::before { 77 | opacity: 0.5; 78 | } 79 | 80 | .icon { 81 | opacity: 1; 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /gardenhub/static/less/components/_welcome_screen.less: -------------------------------------------------------------------------------- 1 | body.home .content { 2 | padding: 0; 3 | } 4 | 5 | .welcome-screen { 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | flex-direction: column; 10 | padding: 50px 0 40px 0; 11 | 12 | background-image: url("/static/images/sunrise.svg"); 13 | background-position: center 20px; 14 | background-repeat: no-repeat; 15 | 16 | > .ui.container { 17 | display: flex; 18 | justify-content: center; 19 | } 20 | 21 | } 22 | 23 | .ui.items > .item > .huge.icons.image:not(.ui) { 24 | width: auto; 25 | } 26 | .ui.unstackable.items > .item > .huge.icons.image { 27 | width: auto !important; 28 | } 29 | 30 | .welcome-screen .huge.icons .order.icon + .corner.icon { 31 | text-shadow: -1px -1px 0 #fff, 32 | 1px -1px 0 #fff, 33 | -1px 1px 0 #fff, 34 | 1px 1px 0 #fff, 35 | -2px -2px 0 #fff, 36 | 2px -2px 0 #fff, 37 | -2px 2px 0 #fff, 38 | 2px 2px 0 #fff, 39 | -2px -1px 0 #fff, 40 | 2px -1px 0 #fff, 41 | -2px 1px 0 #fff, 42 | 2px 1px 0 #fff, 43 | -3px -3px 0 #fff, 44 | 3px -3px 0 #fff, 45 | -3px 3px 0 #fff, 46 | 3px 3px 0 #fff, 47 | -3px -2px 0 #fff, 48 | 3px -2px 0 #fff, 49 | -3px 2px 0 #fff, 50 | 3px 2px 0 #fff, 51 | -3px -1px 0 #fff, 52 | 3px -1px 0 #fff, 53 | -3px 1px 0 #fff, 54 | 3px 1px 0 #fff, 55 | } 56 | 57 | .welcome-screen .ui.items > .item { 58 | position: relative; 59 | &:hover a { 60 | color: #1e70bf; 61 | } 62 | } 63 | 64 | a.full { 65 | position: absolute; 66 | top: 0; 67 | left: 0; 68 | right: 0; 69 | bottom: 0; 70 | width: 100%; 71 | height: 100%; 72 | z-index: 1; 73 | } 74 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | MAINTAINER HarvestHub 4 | 5 | # Makes manage.py commands able to show output 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | ADD requirements.txt /requirements.txt 9 | 10 | RUN set -ex \ 11 | # Install build dependencies 12 | && apk add --no-cache --virtual .build-deps \ 13 | # General build dependencies 14 | python-dev \ 15 | build-base \ 16 | linux-headers \ 17 | pcre-dev \ 18 | # Pillow build depenencies 19 | jpeg-dev \ 20 | zlib-dev \ 21 | freetype-dev \ 22 | lcms2-dev \ 23 | openjpeg-dev \ 24 | tiff-dev \ 25 | tk-dev \ 26 | tcl-dev \ 27 | # Postgres build dependencies 28 | postgresql-dev \ 29 | # Upgrade pip 30 | && pip install -U pip \ 31 | # Install requirements.txt 32 | && pip install --no-cache-dir -r /requirements.txt \ 33 | # Install gunicorn 34 | && pip install --no-cache-dir gunicorn \ 35 | # Scans project and collects runtime dependencies 36 | && runDeps="$( \ 37 | scanelf --needed --nobanner --recursive \ 38 | $(python -c 'import site; print(site.getsitepackages()[0])') \ 39 | | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ 40 | | sort -u \ 41 | | xargs -r apk info --installed \ 42 | | sort -u \ 43 | )" \ 44 | # Install Node and Less for compiling .less in development 45 | && apk add --no-cache npm \ 46 | && npm install -g less \ 47 | # Add the runtime dependencies we need to keep 48 | && apk add --virtual .python-rundeps $runDeps \ 49 | # Delete the build dependencies we no longer need 50 | && apk del .build-deps 51 | 52 | WORKDIR /app 53 | ADD . /app 54 | 55 | RUN set -ex \ 56 | # Collect static files for production 57 | && python manage.py collectstatic --noinput \ 58 | # Compile static files for production 59 | && python manage.py compress --force 60 | 61 | CMD ["gunicorn", "wsgi", "-b :5000"] 62 | -------------------------------------------------------------------------------- /settings/production.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .base import * # noqa 4 | 5 | # Do not set SECRET_KEY, Postgres or LDAP password or any sensitive data here. 6 | # Instead, use environment variables or create a local.py file on the server. 7 | 8 | # Disable debug mode 9 | DEBUG = False 10 | TEMPLATES[0]['OPTIONS']['debug'] = False # noqa 11 | 12 | env = os.environ.copy() 13 | 14 | if 'SECRET_KEY' in env: 15 | SECRET_KEY = env['SECRET_KEY'] 16 | 17 | if 'ALLOWED_HOSTS' in env: 18 | ALLOWED_HOSTS = env['ALLOWED_HOSTS'].split(',') 19 | 20 | # Configure email 21 | # https://docs.djangoproject.com/en/2.0/topics/email/ 22 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 23 | EMAIL_USE_TLS = True 24 | EMAIL_HOST = env['EMAIL_HOST'] 25 | EMAIL_PORT = int(env.get('EMAIL_PORT', 587)) 26 | EMAIL_HOST_USER = env['EMAIL_HOST_USER'] 27 | EMAIL_HOST_PASSWORD = env['EMAIL_HOST_PASSWORD'] 28 | DEFAULT_FROM_EMAIL = env['DEFAULT_FROM_EMAIL'] 29 | 30 | LOGGING = { 31 | 'version': 1, 32 | 'disable_existing_loggers': False, 33 | 'handlers': { 34 | 'console': { 35 | 'class': 'logging.StreamHandler', 36 | }, 37 | }, 38 | 'loggers': { 39 | 'django': { 40 | 'handlers': ['console'], 41 | 'level': os.getenv('DJANGO_LOG_LEVEL', 'ERROR'), 42 | }, 43 | }, 44 | } 45 | 46 | # Makes WhiteNoise work properly. 47 | # http://whitenoise.evans.io/en/stable/django.html#django-compressor 48 | COMPRESS_OFFLINE = True 49 | 50 | # S3 file storage for media uploads 51 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html 52 | DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' 53 | AWS_ACCESS_KEY_ID = env['AWS_ACCESS_KEY_ID'] 54 | AWS_SECRET_ACCESS_KEY = env['AWS_SECRET_ACCESS_KEY'] 55 | AWS_STORAGE_BUCKET_NAME = env['AWS_STORAGE_BUCKET_NAME'] 56 | AWS_S3_ENDPOINT_URL = env['AWS_S3_ENDPOINT_URL'] 57 | AWS_S3_SIGNATURE_VERSION = 's3v4' 58 | 59 | try: 60 | from .local import * # noqa 61 | except ImportError: 62 | pass 63 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/plot_list.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | {% load gardenhub thumbnail %} 3 | 4 | {% block content %} 5 | {% if request.user.is_garden_manager %} 6 | 7 | 8 | New Plot 9 | 10 | {% endif %} 11 | 12 |

Manage Plots

13 | 14 |
15 | {% for plot in plots %} 16 |
17 |
18 | 19 |
{{ plot.garden.title }}, Plot #{{ plot.title }}
20 |
21 |
22 |

Open Orders

23 |
24 | {% plot_user_orders plot as orders %} 25 | {{ orders.open|length }} open order{{ orders.open|pluralize }}. 26 |
27 | 28 |

Crops in this Plot

29 |
30 | {% for crop in plot.crops.all %} 31 |
32 | {% thumbnail crop.image "50x50" crop="center" as photo %} 33 | 34 | {% endthumbnail %} 35 |
36 |
{{ crop.title }}
37 |
38 |
39 | {% endfor %} 40 |
41 |
42 | 43 | 44 | Edit Plot 45 | 46 |
47 | {% endfor %} 48 |
49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/account_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | {% load thumbnail gardenhub %} 3 | 4 | {% block content %} 5 | 6 |
7 | 18 |
19 | 20 | {% if request.user.photo %} 21 | {% thumbnail request.user.photo "300x300" crop="center" as photo %} 22 | 23 | {% endthumbnail %} 24 | {% else %} 25 | 26 | {% endif %} 27 |

28 | {{user.first_name}} {{user.last_name}} 29 |

30 | 31 |
32 |
33 |
34 | 35 | 38 |
39 | {% if request.user.phone_number %} 40 |
41 | 42 | 45 |
46 | {% endif %} 47 |
48 |
49 | 50 |
51 | 52 | 53 | Edit Profile 54 | 55 |
56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /gardenhub/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_base.html" %} 2 | {% load static %} 3 | 4 | {% block body_class %}login{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

10 | 11 | GardenHub 12 |

13 | {% include "gardenhub/partials/messages.html" %} 14 |
15 |
{{ form.non_field_errors|first }}
16 | {% csrf_token %} 17 |
18 |
19 | 20 |
21 | 22 | 23 |
24 | {{ form.username.errors }} 25 |
26 |
27 | 28 |
29 | 30 | 31 |
32 | {{ form.password.errors }} 33 |
34 | 35 |
36 | Reset your password 37 |
38 |
39 |
40 | {% endblock %} 41 | 42 | {% block extra_scripts %} 43 | 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | # Run containers 4 | function start() { 5 | docker-compose run --rm web python manage.py migrate 6 | docker-compose up 7 | } 8 | 9 | # Pull database from staging to local 10 | function pulldb() { 11 | ssh dokku@candlewaster.co postgres:export gardenhub > db.dump 12 | docker-compose down -v 13 | docker-compose up -d 14 | docker-compose run --rm -v $(pwd)/db.dump:/db.dump db sh -c \ 15 | "pg_restore -U postgres -h db -d postgres /db.dump" 16 | docker-compose down 17 | echo "Successfully restored staging database!" 18 | } 19 | 20 | # Pull media files from staging 21 | function pullmedia() { 22 | mkdir -p media 23 | ssh gardenhub@candlewaster.co s3cmd get s3://gardenhub/gardenhub --recursive --skip-existing 24 | rsync -av gardenhub@candlewaster.co:~/gardenhub/ ./media 25 | ssh gardenhub@candlewaster.co rm -r gardenhub 26 | } 27 | 28 | # Serve docs 29 | function servedocs() { 30 | docker run --rm -it \ 31 | -p 8000:8000 \ 32 | --network=host \ 33 | -e PYTHONUNBUFFERED=0 \ 34 | -v $(pwd):/app \ 35 | $app sh -c "cd /app/docs && sphinx-autobuild -b html . _build/html" 36 | } 37 | 38 | # Options 39 | case $1 in 40 | setup) 41 | sudo sh -c "wget -nv -O - https://get.docker.com/ | sh" 42 | ;; 43 | build) docker-compose build ;; 44 | start) start ;; 45 | manage.py) 46 | shift 47 | docker-compose run --rm web python manage.py $@ 48 | ;; 49 | docs) servedocs ;; 50 | pulldb) pulldb ;; 51 | pullmedia) pullmedia ;; 52 | *) 53 | echo "$app local development script" 54 | echo "" 55 | echo "usage: ./dev.sh []" 56 | echo "" 57 | echo "Commands:" 58 | echo " start Run the app for local development on port 5000." 59 | echo " build Manually (re)build the app container." 60 | echo " manage.py Runs python manage.py in the app container." 61 | echo " setup Installs Docker." 62 | echo " docs Runs a local server for the docs on port 5000." 63 | echo "" 64 | echo "Staging sync (permission required):" 65 | echo " pulldb Downloads db from staging." 66 | echo " pullmedia Downloads media files from staging." 67 | esac 68 | -------------------------------------------------------------------------------- /docs/developers/deployment.rst: -------------------------------------------------------------------------------- 1 | Deployment (WIP) 2 | ========== 3 | 4 | This guide will walk you through deploying an instance of GardenHub online. It will make a lot of choices for you, with the trade-off of not requiring advanced knowledge in order to do this. Familiarity with the Linux terminal is required. 5 | 6 | 1. Getting the server 7 | --------------------- 8 | 9 | If you don't have one, create an account with DigitalOcean_. Then **create a new droplet.** It will cost you at least $10/mo, possibly more depending on the size of your userbase. 10 | 11 | * You will want to select the latest LTS, 64-bit Ubuntu as your OS. At the time of writing, that is ``16.04 x64``. The LTS version is the one that ends in ``.04``, not ``.10``. 12 | * Choose **at least 2GB of memory** if you intend to run this application seriously. If not, you may be able to get away with less. With many users, you will want to increase the memory so the app doesn't become slow. You can always do this later. 13 | * Choose a datacenter region that is close to the majority of your users, if possible. 14 | * Enable IPv6. 15 | * Set the hostname to the domain name you intend to use, such as ``gardenhub.io``. 16 | * You can leave the other options alone. Click "Create". 17 | 18 | Next, wait a minute for the droplet to provision. Once it's done, you'll get an IP address. You can use that to shell into the server via your terminal. Replace the IP address with your droplet's: 19 | 20 | .. code-block:: bash 21 | 22 | ssh root@765.432.1 23 | 24 | The password will be provided by DigitalOcean. 25 | 26 | 2. Provisioning the server 27 | -------------------------- 28 | 29 | First, let's install Dokku_. It will let us push the GardenHub repo up to the server via git. While ssh'd into the server, run this: 30 | 31 | .. code-block:: bash 32 | 33 | wget https://raw.githubusercontent.com/dokku/dokku/v0.11.3/bootstrap.sh 34 | sudo DOKKU_TAG=v0.11.3 bash bootstrap.sh 35 | 36 | Once it completes, visit your server's IP address in your web browser and follow the instructions. Be sure to tick the virtual hosts option and enter the domain name you intend to use with the app. 37 | 38 | TODO: Write the rest of this 39 | 40 | 41 | .. _DigitalOcean: https://www.digitalocean.com/ 42 | .. _Dokku: http://dokku.viewdocs.io/dokku/ 43 | -------------------------------------------------------------------------------- /gardenhub/static/fonts/src/garden-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | Fence 15 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | Fence 26 | 28 | https://thenounproject.com/icon/1179077/ 29 | 30 | 31 | Daniela Baptista 32 | 33 | 34 | 35 | 37 | 39 | 41 | 43 | 45 | 47 | 48 | 49 | 50 | 54 | 55 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/garden_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | {% load gardenhub thumbnail %} 3 | 4 | {% block content %} 5 | 6 |
7 | 19 |
20 | 21 | {% if garden.photo %} 22 | {% thumbnail garden.photo "300x300" crop="center" as photo %} 23 | 24 | {% endthumbnail %} 25 | {% else %} 26 | 27 | {% endif %} 28 | 29 |

30 | {{ garden.title }} 31 |

32 | 33 |
34 |
35 |
36 | 37 |
38 | {{ garden.plots.all|length }} plot{{ garden.plots.all|pluralize }} 39 |
40 |
41 | {% if garden.affiliations.all %} 42 |
43 | 44 |
45 | Member of: 46 | {% for affiliation in garden.affiliations.all %} 47 | {{ affiliation.title }} 48 | {% endfor %} 49 |
50 |
51 | {% endif %} 52 |
53 | 54 |
55 | {{ garden.address }} 56 |
57 |
58 | {% if garden.map_image %} 59 |
60 | 61 |
62 | View map 63 |
64 |
65 | {% endif %} 66 |
67 |
68 | 69 | 74 | {% endblock %} 75 | -------------------------------------------------------------------------------- /docs/users/invite.rst: -------------------------------------------------------------------------------- 1 | Inviting new users 2 | ================== 3 | 4 | For now, GardenHub is an invite-only platform. Anyone can invite other people to co-manage resources they can edit (such as plots or gardens). To invite a new user, you must first create the plot or garden you would like that user to manage, then add that user's email address into the "managers" (or "gardeners") field. 5 | 6 | Email invitations only work in the `client view`_. 7 | 8 | Invite a new user to a plot 9 | --------------------------- 10 | 11 | First, create a new plot, or edit an existing plot. Type the email address of the user you'd like to invite into the "gardeners" field. You will need to click away after so the email becomes enclosed in a gray box as shown below. Then you may click "Save plot" and an invitation will be sent to the email address(es) you entered. 12 | 13 | Administrators must first `add themselves to the plot`_ before they can edit it in the client view. 14 | 15 | .. image:: /_static/invite-user-plot.png 16 | 17 | Invite a new user to a garden 18 | ----------------------------- 19 | 20 | First, create a new garden, or edit an existing garden. Type the email address of the user you'd like to invite into the "managers" field. You will need to click away after so the email becomes enclosed in a gray box as shown below. Then you may click "Save" and an invitation will be sent to the email address(es) you entered. 21 | 22 | Administrators must first `add themselves to the garden`_ before they can edit it in the client view. 23 | 24 | .. image:: /_static/invite-user-garden.png 25 | 26 | Create a new user manually 27 | -------------------------- 28 | 29 | If you'd like to create a new user without inviting them to a plot or garden, you can add them manually. They won't receive an email invitation, so you will need to send them the password you choose. 30 | 31 | First, visit the admin panel. You'll need to be a superuser or staff user to do this: 32 | 33 | .. image:: /_static/admin-button.png 34 | 35 | Next, click the "Add" button next to "Users": 36 | 37 | .. image:: /_static/admin-add-user.png 38 | 39 | Fill out the form. **You must tick the "Active" box to allow the user to log in.** You may also tick "Staff status" and "Superuser status" to grant this user admin privileges. 40 | 41 | .. image:: /_static/admin-user-form.png 42 | 43 | Finally, click "Save". The user you created will now be able to log in. Be sure to assign them to any plots or gardens you'd like them to manage. 44 | 45 | .. _`client view`: intro.html#client-view 46 | .. _`add themselves to the plot`: manage.html#adding-yourself-to-a-plot 47 | .. _`add themselves to the garden`: manage.html#adding-yourself-to-a-garden 48 | -------------------------------------------------------------------------------- /gardenhub/management/commands/_crops_list.py: -------------------------------------------------------------------------------- 1 | crops_list = [ 2 | 'acorn squash', 3 | 'alfalfa spouts', 4 | 'amaranth', 5 | 'apricots', 6 | 'artichoke', 7 | 'asparagus', 8 | 'avocado', 9 | 'bananas', 10 | 'barley', 11 | 'basil', 12 | 'bay leaf', 13 | 'bean sprouts', 14 | 'beets', 15 | 'blackberries', 16 | 'blueberries', 17 | 'bok choy', 18 | 'broccoli', 19 | 'brussels sprouts', 20 | 'butternut squash', 21 | 'cabbage', 22 | 'caper', 23 | 'carrots', 24 | 'cauliflower', 25 | 'cayenne peppers', 26 | 'celery', 27 | 'chamomile', 28 | 'cherries', 29 | 'cherry tomatoes', 30 | 'chestnuts', 31 | 'chickpeas', 32 | 'chives', 33 | 'cilantro', 34 | 'collard greens', 35 | 'coriander', 36 | 'corn', 37 | 'cranberries', 38 | 'cucumber', 39 | 'daisy', 40 | 'daylily', 41 | 'dill', 42 | 'eggplant', 43 | 'elderberries', 44 | 'fennel', 45 | 'garlic', 46 | 'ginger', 47 | 'gourds', 48 | 'grapefruit', 49 | 'green apples', 50 | 'green beans', 51 | 'green grapes', 52 | 'green pepper', 53 | 'habanero pepper', 54 | 'honeydew', 55 | 'hops', 56 | 'hot peppers', 57 | 'huckleberries', 58 | 'jalapeno peppers', 59 | 'kale', 60 | 'kiwi', 61 | 'kohlrabi', 62 | 'leeks', 63 | 'lentils', 64 | 'lettuce', 65 | 'luffa', 66 | 'marigold', 67 | 'mustard', 68 | 'okra', 69 | 'olives', 70 | 'onion', 71 | 'oranges', 72 | 'oregano', 73 | 'oyster mushrooms', 74 | 'parsley', 75 | 'peach', 76 | 'peanuts', 77 | 'pears', 78 | 'peas', 79 | 'peppermint', 80 | 'pineapple', 81 | 'portobello mushrooms', 82 | 'potatoes', 83 | 'pumpkin', 84 | 'purple grapes', 85 | 'quinoa', 86 | 'radish', 87 | 'raspberries', 88 | 'red apple', 89 | 'red lettuce', 90 | 'red onions', 91 | 'red pepper', 92 | 'rosemary', 93 | 'sage', 94 | 'scallions', 95 | 'sesame', 96 | 'shallots', 97 | 'shitake mushrooms', 98 | 'snap peas', 99 | 'snow peas', 100 | 'soy beans', 101 | 'spinach', 102 | 'starfruit', 103 | 'strawberries', 104 | 'sunflower', 105 | 'sweet corn', 106 | 'sweet potatoes', 107 | 'thai basil', 108 | 'thyme', 109 | 'tomatillo', 110 | 'tomatoes', 111 | 'truffle', 112 | 'tulips', 113 | 'walnuts', 114 | 'watermelon', 115 | 'white onion', 116 | 'yams', 117 | 'yellow onion', 118 | 'yellow peppers', 119 | 'zucchini' 120 | ] 121 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/_base.html: -------------------------------------------------------------------------------- 1 | 2 | {% load static %} 3 | {% load compress %} 4 | 5 | 6 | GardenHub 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% compress css %} 31 | 32 | {% endcompress %} 33 | {% block extra_styles %}{% endblock %} 34 | 35 | 36 | {% block header %}{% endblock %} 37 | {% block wrap %} 38 | {% block content %}{% endblock %} 39 | {% endblock %} 40 | 41 | 42 | 43 | 44 | 45 | {% compress js %} 46 | 47 | {% endcompress %} 48 | {% block extra_scripts %}{% endblock %} 49 | 50 | 51 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/_manage_base.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_base.html" %} 2 | {% load static thumbnail gardenhub %} 3 | 4 | {% block primary_body_class %}has-top-menu{% endblock %} 5 | 6 | {% block header %} 7 | 54 | {% endblock %} 55 | 56 | {% block wrap %} 57 |
58 |
59 | {% include "gardenhub/partials/messages.html" %} 60 | {% block content %}{% endblock %} 61 |
62 |
63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /gardenhub/test_views.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, Client 2 | from django.urls import reverse 3 | from gardenhub.models import Plot 4 | from gardenhub.factories import ( 5 | GardenFactory, ActiveUserFactory, GardenerFactory, GardenManagerFactory, 6 | CropFactory 7 | ) 8 | 9 | 10 | class PlotCreateViewTestCase(TestCase): 11 | 12 | def test_get(self): 13 | """ Ensure that the page can be accessed """ 14 | client = Client() 15 | # Only GM's can access the page 16 | client.force_login(GardenManagerFactory()) 17 | response = client.get(reverse('plot-create')) 18 | self.assertEqual(response.status_code, 200) 19 | 20 | def test_access(self): 21 | """ Only GM's can access the page """ 22 | client = Client() 23 | # Normal users can't access 24 | client.force_login(ActiveUserFactory()) 25 | response = client.get(reverse('plot-create')) 26 | self.assertEqual(response.status_code, 403) 27 | 28 | def test_post(self): 29 | """ Test submit under ideal conditions """ 30 | client = Client() 31 | 32 | manager = GardenManagerFactory() 33 | garden = manager.get_gardens().first() 34 | client.force_login(manager) 35 | 36 | response = client.post( 37 | reverse('plot-create'), { 38 | 'garden': garden.id, 39 | 'title': '0', 40 | 'gardener_emails': manager.email, 41 | 'crops': CropFactory().id 42 | } 43 | ) 44 | 45 | # Test status code 46 | self.assertRedirects(response, reverse('plot-list')) 47 | 48 | # Test that object was created 49 | self.assertTrue(Plot.objects.get(title=0)) 50 | 51 | 52 | class PlotUpdateViewTestCase(TestCase): 53 | 54 | def test_post(self): 55 | """ Test that the form submits """ 56 | client = Client() 57 | 58 | gardener = GardenerFactory() 59 | plot = gardener.get_plots().first() 60 | client.force_login(gardener) 61 | 62 | # Add user to multiple gardens 63 | # https://github.com/HarvestHub/GardenHub/issues/88 64 | plot.garden.managers.add(gardener) 65 | GardenFactory(managers=[gardener]) 66 | 67 | response = client.post( 68 | reverse('plot-update', args=[plot.id]), { 69 | 'garden': plot.garden.id, 70 | 'title': "New title", 71 | 'gardener_emails': gardener.email, 72 | 'crops': CropFactory().id 73 | } 74 | ) 75 | 76 | # Ensure the correct response 77 | self.assertRedirects(response, reverse('plot-list')) 78 | 79 | # Check that the model updated 80 | plot.refresh_from_db() 81 | self.assertEqual(plot.title, "New title") 82 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/garden_form.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | {% load gardenhub %} 3 | 4 | {% block content %} 5 |
6 | {% csrf_token %} 7 | {% if form.non_field_errors %} 8 |
{{ form.non_field_errors }}
9 | {% endif %} 10 | 11 |
12 | 13 |

Enter the name of this garden.

14 |
15 | 16 |
17 | {{ form.title.errors }} 18 |
19 | 20 |
21 | 22 |

Enter the address of this garden.

23 |
24 | 25 | 26 |
27 | {{ form.address.errors }} 28 |
29 | 30 |
31 | 32 |

Upload a map of the garden. (jpg/png)

33 |
34 | {{ form.map_image }} 35 |
36 | {{ form.map_image.errors }} 37 |
38 | 39 |
40 | 41 |

List the people who should manage this garden, or enter an email address to invite someone new.

42 | 48 | {{ form.manager_emails.errors }} 49 |
50 | 51 | 55 |
56 | {% endblock %} 57 | 58 | {% block extra_scripts %} 59 | 71 | {% endblock %} 72 | -------------------------------------------------------------------------------- /gardenhub/urls.py: -------------------------------------------------------------------------------- 1 | """gardenhub URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.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.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.urls import path 18 | from django.conf.urls.static import static 19 | from gardenhub import views 20 | from django.contrib import admin 21 | from django.contrib.auth.views import LoginView 22 | 23 | 24 | urlpatterns = [ 25 | path('', views.HomePageView.as_view(), name='home'), 26 | path('admin/', admin.site.urls), 27 | # Auth 28 | path('login/', LoginView.as_view(), name='login'), 29 | path('logout/', views.LogoutView.as_view(), name='logout'), 30 | path('password-reset/', views.PasswordResetView.as_view(), name='password_reset'), # noqa 31 | path('password-reset///', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), # noqa 32 | # Account 33 | path('account/', views.AccountView.as_view(), name='account'), 34 | path('account/settings/', views.AccountSettingsView.as_view(), name='account-settings'), # noqa 35 | path('account/remove/', views.AccountRemoveView.as_view(), name='account-remove'), # noqa 36 | path('activate//', views.account_activate_view, name='account-activate'), # noqa 37 | # Orders 38 | path('orders/', views.OrderListView.as_view(), name='order-list'), 39 | path('orders/new/', views.OrderCreateView.as_view(), name='order-create'), 40 | path('order//', views.OrderDetailView.as_view(), name='order-detail'), # noqa 41 | path('order//cancel/', views.OrderCancelView.as_view(), name='order-cancel'), # noqa 42 | path('pick//', views.PickCreateView.as_view(), name='pick-create'), # noqa 43 | # Plots 44 | path('plots/', views.PlotListView.as_view(), name='plot-list'), 45 | path('plots/new/', views.PlotCreateView.as_view(), name='plot-create'), 46 | path('plot//edit/', views.PlotUpdateView.as_view(), name='plot-update'), # noqa 47 | # Gardens 48 | path('gardens/', views.GardenListView.as_view(), name='garden-list'), 49 | path('garden//', views.GardenDetailView.as_view(), name='garden-detail'), # noqa 50 | path('garden//edit/', views.GardenUpdateView.as_view(), name='garden-update'), # noqa 51 | # API 52 | path('_api/crops//', views.ApiCrops.as_view()), 53 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 54 | -------------------------------------------------------------------------------- /gardenhub/forms.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from django import forms 3 | from django.core import validators 4 | from django.core.exceptions import ValidationError 5 | from phonenumber_field.formfields import PhoneNumberField 6 | from gardenhub.models import Order, Garden, Plot, Crop 7 | from gardenhub.utils import today, localdate 8 | 9 | 10 | class MultipleEmailField(forms.MultipleChoiceField): 11 | def clean(self, value): 12 | values = super().clean(value) 13 | emails = [email.lower() for email in values] 14 | return list(set(emails)) 15 | 16 | def valid_value(self, value): 17 | # Valid values are email addresses 18 | try: 19 | validators.validate_email(value) 20 | return True 21 | except ValidationError: 22 | return False 23 | 24 | 25 | class OrderForm(forms.ModelForm): 26 | class Meta: 27 | model = Order 28 | fields = ['plot', 'start_date', 'end_date', 29 | 'pick_all', 'crops', 'comment'] 30 | 31 | def clean_start_date(self): 32 | start_date = self.cleaned_data['start_date'] 33 | 34 | # Prevent orders with a start_date before today 35 | if localdate(start_date) < today(): 36 | raise ValidationError("You cannot create a backdated order") 37 | 38 | # Prevent orders with less than a day's notice 39 | if localdate(start_date) < today() + timedelta(days=1): 40 | raise ValidationError("Please allow 24 hours between now " 41 | "and the start of your order") 42 | return start_date 43 | 44 | def clean(self): 45 | cleaned_data = super().clean() 46 | pick_all = cleaned_data["pick_all"] 47 | crops = cleaned_data["crops"] 48 | 49 | if not pick_all and not crops: 50 | raise forms.ValidationError( 51 | "Please indicate which crops you'd like picked.") 52 | 53 | if pick_all is True: 54 | cleaned_data["crops"] = Crop.objects.none() 55 | 56 | return cleaned_data 57 | 58 | 59 | class PlotForm(forms.ModelForm): 60 | gardener_emails = MultipleEmailField(required=False) 61 | 62 | class Meta: 63 | model = Plot 64 | fields = ['title', 'garden', 'crops'] 65 | 66 | 67 | class GardenForm(forms.ModelForm): 68 | manager_emails = MultipleEmailField(required=False) 69 | 70 | class Meta: 71 | model = Garden 72 | fields = ['title', 'address', 'map_image'] 73 | 74 | 75 | class ActivateAccountForm(forms.Form): 76 | first_name = forms.CharField() 77 | last_name = forms.CharField() 78 | phone_number = PhoneNumberField(required=False) 79 | password1 = forms.CharField() 80 | password2 = forms.CharField() 81 | 82 | 83 | class AccountSettingsForm(forms.Form): 84 | first_name = forms.CharField() 85 | last_name = forms.CharField() 86 | phone_number = PhoneNumberField(required=False) 87 | photo = forms.ImageField(required=False) 88 | password = forms.CharField(required=False) 89 | new_password1 = forms.CharField(required=False) 90 | new_password2 = forms.CharField(required=False) 91 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/account_activate.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_base.html" %} 2 | 3 | {% block content %} 4 |
5 |

6 | Let's set you up 7 | 8 | Enter your details to begin. 9 | 10 |

11 |
12 | {% csrf_token %} 13 | {% if form.non_field_errors %} 14 |
{{ form.non_field_errors }}
15 | {% endif %} 16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 | {{ form.first_name.errors }} 24 |
25 |
26 | 27 |
28 | 29 |
30 | {{ form.last_name.errors }} 31 |
32 |
33 |
34 | 35 |
36 | 37 |
38 | {{ form.phone_number.errors }} 39 |
40 |
41 |
42 |
43 | 44 |
45 | 46 |
47 | {{ form.password1.errors }} 48 |
49 |
50 | 51 |
52 | 53 |
54 | {{ form.password2.errors }} 55 |
56 |
57 | 61 |
62 |
63 | {% endblock %} 64 | 65 | {% block extra_styles %} 66 | 71 | {% endblock %} 72 | 73 | {% block extra_scripts %} 74 | 84 | {% endblock %} 85 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/pick_form.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | {% load static %} 3 | 4 | {% block content %} 5 | 6 | {% if form.non_field_errors %} 7 |
{{ form.non_field_errors }}
8 | {% endif %} 9 | 10 |

11 | Today I picked… 12 | Crops from plot {{ plot.title }} in {{ plot.garden.title }} 13 |

14 | 15 | {% with order=plot.order_set.active.first %} 16 | {% if order %} 17 | {% with crops=order.crops.all %} 18 |

Crops to pick

19 | {% if order.pick_all %} 20 |

Pick all the crops on the plot.

21 | {% elif crops %} 22 |
23 | {% for crop in crops %} 24 |
25 | 26 |
27 |
{{ crop.title }}
28 |
29 |
30 | {% endfor %} 31 |
32 | {% else %} 33 |

This order has no crops in it. Something is wrong.

34 | {% endif %} 35 | {% endwith %} 36 | 37 | {% if order.comment %} 38 |
39 |
40 | Special Instructions 41 |
42 | “{{ order.comment }}” —{{ order.requester.get_full_name }} 43 |
44 | {% endif %} 45 | {% endif %} 46 | {% endwith %} 47 | 48 |
49 | {% csrf_token %} 50 |
51 | 52 |

Type the names of the crops you picked today from plot {{ plot.title }}.

53 | 59 | {{ form.crops.errors }} 60 |
61 |
62 | 63 |

Any additional information about this pick.

64 | 65 |
66 | 70 |
71 | {% endblock %} 72 | 73 | {% block extra_scripts %} 74 | 86 | {% endblock %} 87 | -------------------------------------------------------------------------------- /gardenhub/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at harvesthubphilly@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/account_settings.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | 3 | {% block content %} 4 |

5 | Account Settings 6 | Manage your account. 7 |

8 | 9 |
10 | {% csrf_token %} 11 | {% if form.non_field_errors %} 12 |
{{ form.non_field_errors }}
13 | {% endif %} 14 |

Profile

15 |
16 | 17 |
18 | 19 |
20 | {{ form.first_name.errors }} 21 |
22 |
23 | 24 |
25 | 26 |
27 | {{ form.last_name.errors }} 28 |
29 |
30 | 31 |
32 | 33 |
34 | {{ form.phone_number.errors }} 35 |
36 |
37 | 38 |
39 | {{ form.photo }} 40 |
41 | {{ form.photo.errors }} 42 |
43 | 44 |

Change Password

45 |

You don't need to fill out this section unless you want to change your password.

46 |
47 | 48 |
49 | 50 |
51 | {{ form.password.errors }} 52 |
53 |
54 | 55 |
56 | 57 |
58 | {{ form.new_password1.errors }} 59 |
60 |
61 | 62 |
63 | 64 |
65 | {{ form.new_password2.errors }} 66 |
67 | 68 | 72 |
73 | {% endblock %} 74 | 75 | {% block extra_scripts %} 76 | 84 | {% endblock %} 85 | -------------------------------------------------------------------------------- /gardenhub/static/images/logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /gardenhub/static/fonts/src/plot-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | Community Garden Plot Icon 15 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | Community Garden Plot Icon 26 | 28 | https://openclipart.org/detail/296147/community-garden-plot-icon 29 | 2018-02-07 30 | 31 | 32 | Alex Gleason 33 | 34 | 35 | 36 | 38 | 40 | 42 | 44 | 45 | 46 | 47 | 51 | 52 | -------------------------------------------------------------------------------- /gardenhub/static/images/logo-small.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /gardenhub/factories.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | import factory 3 | from factory.django import DjangoModelFactory 4 | from gardenhub.models import Garden, Plot, Crop, Order, Pick 5 | 6 | 7 | class CropFactory(DjangoModelFactory): 8 | class Meta: 9 | model = Crop 10 | 11 | title = factory.Sequence(lambda n: "Crop #%s" % n) 12 | image = factory.django.ImageField() 13 | 14 | 15 | class GardenFactory(DjangoModelFactory): 16 | class Meta: 17 | model = Garden 18 | 19 | title = factory.Sequence(lambda n: "Garden #%s" % n) 20 | address = factory.Sequence(lambda n: "%s GardenHub Avenue" % n) 21 | photo = factory.django.ImageField() 22 | 23 | @factory.post_generation 24 | def managers(self, create, extracted, **kwargs): 25 | if extracted: 26 | self.managers.set(extracted) 27 | 28 | @factory.post_generation 29 | def pickers(self, create, extracted, **kwargs): 30 | if extracted: 31 | self.pickers.set(extracted) 32 | 33 | 34 | class PlotFactory(DjangoModelFactory): 35 | class Meta: 36 | model = Plot 37 | 38 | title = factory.Sequence(lambda n: str(n)) 39 | garden = factory.SubFactory(GardenFactory) 40 | 41 | @factory.post_generation 42 | def crop_count(self, create, extracted, **kwargs): 43 | if extracted and not kwargs.get('crops'): 44 | self.crops.set([CropFactory() for _ in range(extracted)]) 45 | 46 | @factory.post_generation 47 | def crops(self, create, extracted, **kwargs): 48 | if extracted: 49 | self.crops.set(extracted) 50 | 51 | @factory.post_generation 52 | def gardeners(self, create, extracted, **kwargs): 53 | if extracted: 54 | self.gardeners.set(extracted) 55 | 56 | 57 | class ActiveUserFactory(DjangoModelFactory): 58 | class Meta: 59 | model = get_user_model() 60 | 61 | first_name = factory.Faker('first_name') 62 | last_name = factory.Faker('last_name') 63 | email = factory.LazyAttribute( 64 | lambda user: '{}.{}@gardenhub.dev'.format( 65 | user.first_name, user.last_name 66 | ).lower()) 67 | photo = factory.django.ImageField() 68 | is_active = True 69 | 70 | 71 | class GardenerFactory(ActiveUserFactory): 72 | @factory.post_generation 73 | def plots(self, create, extracted, **kwargs): 74 | if extracted: 75 | # Set to plots if provided 76 | for plot in extracted: 77 | plot.gardeners.add(self) 78 | else: 79 | # Otherwise, add to a single plot 80 | PlotFactory().gardeners.add(self) 81 | 82 | 83 | class GardenManagerFactory(ActiveUserFactory): 84 | @factory.post_generation 85 | def gardens(self, create, extracted, **kwargs): 86 | if extracted: 87 | # Set to gardens if provided 88 | for garden in extracted: 89 | garden.managers.add(self) 90 | else: 91 | # Otherwise, add to a single garden 92 | GardenFactory().managers.add(self) 93 | 94 | 95 | class PickerFactory(ActiveUserFactory): 96 | @factory.post_generation 97 | def picker_gardens(self, create, extracted, **kwargs): 98 | if extracted: 99 | # Set gardens if provided 100 | for garden in extracted: 101 | garden.pickers.add(self) 102 | else: 103 | # Otherwise, add to a single garden 104 | GardenFactory().pickers.add(self) 105 | 106 | 107 | class OrderFactory(DjangoModelFactory): 108 | class Meta: 109 | model = Order 110 | 111 | start_date = factory.Faker("date") 112 | end_date = factory.Faker("date") 113 | plot = factory.SubFactory(PlotFactory) 114 | requester = factory.SubFactory(ActiveUserFactory) 115 | 116 | 117 | class PickFactory(DjangoModelFactory): 118 | class Meta: 119 | model = Pick 120 | 121 | picker = factory.SubFactory(PickerFactory) 122 | plot = factory.SubFactory(PlotFactory) 123 | -------------------------------------------------------------------------------- /gardenhub/static/fonts/src/order-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | Accountingimage/svg+xmlAccountinghttps://thenounproject.com/icon/1137184/Symbolon -------------------------------------------------------------------------------- /gardenhub/static/fonts/gardenhub.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/order_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | {% load thumbnail gardenhub %} 3 | 4 | {% block content %} 5 | 6 | {% if not order.canceled and request.user|user_can_edit_order:order %} 7 |
8 | 16 |
17 | {% endif %} 18 | 19 | 20 |

Order #{{ order.id }}

21 | 22 |

23 | 24 | {{ order.plot.garden }}, Plot #{{ order.plot.title }} 25 |

26 | 27 |

Requester

28 |
29 |
30 | 31 | {{ order.requester.get_full_name }} 32 |
33 | 39 | {% if order.requester.phone_number %} 40 | 46 | {% endif %} 47 |
48 | 49 |

Date Range

50 |
51 | {{ order.start_date }}–{{ order.end_date }} 52 |
53 | 54 |

Status

55 | {% include "gardenhub/partials/order_status.html" %} 56 | 57 |

Crops to be Picked

58 | {% if order.pick_all %} 59 |

All crops on the plot

60 | {% elif order.crops %} 61 |
62 | {% for crop in order.crops.all %} 63 |
64 | 65 |
66 |
{{ crop.title }}
67 |
68 |
69 | {% endfor %} 70 |
71 | {% else %} 72 |

No crops were selected for this order.

73 | {% endif %} 74 | 75 | {% if order.comment %} 76 |
77 |
78 | Special Instructions 79 |
80 | “{{ order.comment }}” —{{ order.requester.get_full_name }} 81 |
82 | {% endif %} 83 | 84 | {% if order.get_picks %} 85 |

Activity

86 |
87 | {% for pick in order.get_picks %} 88 |
89 |
90 | {% if pick.picker.photo %} 91 | {% thumbnail pick.picker.photo "150x150" crop="center" as photo %} 92 | 93 | {% endthumbnail %} 94 | {% else %} 95 | 96 | {% endif %} 97 |
98 |
99 |
100 | {{ pick.timestamp }} 101 |
102 |
103 | {{ pick.picker.first_name }} picked {{ pick.crops.all|join:", " }}. 104 |
105 |
106 |
107 | {% endfor %} 108 |
109 | {% endif %} 110 | 111 | {% endblock %} 112 | 113 | {% block extra_scripts %} 114 | 119 | {% endblock %} 120 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/plot_form.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | {% load gardenhub %} 3 | 4 | {% block content %} 5 |
6 | {% csrf_token %} 7 | {% if form.non_field_errors %} 8 |
{{ form.non_field_errors }}
9 | {% endif %} 10 | 11 | {% with field=form.garden.field %} 12 |
13 | 14 |

The garden this plot is in.

15 | 28 | {{ form.garden.errors }} 29 |
30 | {% endwith %} 31 | 32 |
33 | 34 |

The plot's name is probably a number, like 11. The plot should be clearly labeled with a sign.

35 |
36 | 37 |
38 | {{ form.title.errors }} 39 |
40 | 41 |
42 | 43 |

List the people who should manage this plot, or enter an email address to invite someone new.

44 | 57 | {{ form.gardener_emails.errors }} 58 |
59 | 60 |
61 | 62 |

List the crops growing on this plot.

63 | 69 | {{ form.crops.errors }} 70 |
71 | 72 | 76 |
77 | {% endblock %} 78 | 79 | {% block extra_scripts %} 80 | 92 | {% endblock %} 93 | -------------------------------------------------------------------------------- /settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for gardenhub project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/2.0/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/2.0/ref/settings/ 9 | """ 10 | 11 | import os 12 | import dj_database_url 13 | from django.urls import reverse_lazy 14 | 15 | env = os.environ.copy() 16 | 17 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 18 | BASE_DIR = os.path.dirname(os.path.abspath(os.path.join(__file__, '..'))) 19 | 20 | 21 | # Application definition 22 | 23 | INSTALLED_APPS = [ 24 | 'gardenhub', 25 | 'django.contrib.admin', 26 | 'django.contrib.auth', 27 | 'django.contrib.contenttypes', 28 | 'django.contrib.sessions', 29 | 'django.contrib.messages', 30 | 'whitenoise.runserver_nostatic', 31 | 'django.contrib.staticfiles', 32 | 'compressor', 33 | 'sorl.thumbnail', 34 | 'phonenumber_field', 35 | 'storages', 36 | ] 37 | 38 | MIDDLEWARE = [ 39 | 'django.middleware.security.SecurityMiddleware', 40 | 'whitenoise.middleware.WhiteNoiseMiddleware', 41 | 'django.contrib.sessions.middleware.SessionMiddleware', 42 | 'django.middleware.common.CommonMiddleware', 43 | 'django.middleware.csrf.CsrfViewMiddleware', 44 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 45 | 'django.contrib.messages.middleware.MessageMiddleware', 46 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 47 | ] 48 | 49 | ROOT_URLCONF = 'gardenhub.urls' 50 | 51 | TEMPLATES = [ 52 | { 53 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 54 | 'DIRS': [], 55 | 'APP_DIRS': True, 56 | 'OPTIONS': { 57 | 'context_processors': [ 58 | 'django.template.context_processors.debug', 59 | 'django.template.context_processors.request', 60 | 'django.contrib.auth.context_processors.auth', 61 | 'django.contrib.messages.context_processors.messages', 62 | ], 63 | }, 64 | }, 65 | ] 66 | 67 | WSGI_APPLICATION = 'wsgi.application' 68 | 69 | 70 | # Database 71 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 72 | 73 | DATABASES = { 74 | 'default': dj_database_url.config( 75 | conn_max_age=600, 76 | default='postgres://postgres@db:5432/postgres' 77 | ) 78 | } 79 | 80 | 81 | # Password validation 82 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 83 | 84 | AUTH_PASSWORD_VALIDATORS = [ 85 | { 86 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # noqa 87 | }, 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # noqa 90 | }, 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # noqa 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # noqa 96 | }, 97 | ] 98 | 99 | 100 | # Custom user model 101 | # https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#specifying-a-custom-user-model 102 | 103 | AUTH_USER_MODEL = 'gardenhub.User' 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 108 | 109 | LANGUAGE_CODE = 'en-us' 110 | 111 | TIME_ZONE = 'America/New_York' 112 | 113 | USE_I18N = True 114 | 115 | USE_L10N = False 116 | 117 | USE_TZ = True 118 | 119 | 120 | # Static files (CSS, JavaScript, Images) 121 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 122 | 123 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 124 | STATIC_URL = '/static/' 125 | STATICFILES_FINDERS = ( 126 | 'django.contrib.staticfiles.finders.FileSystemFinder', 127 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 128 | 'compressor.finders.CompressorFinder', 129 | ) 130 | COMPRESS_PRECOMPILERS = ( 131 | ('text/less', 'lessc {infile} {outfile}'), 132 | ) 133 | 134 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 135 | MEDIA_URL = '/media/' 136 | 137 | 138 | # Login settings 139 | # https://docs.djangoproject.com/en/2.0/ref/settings/#login-redirect-url 140 | 141 | LOGIN_URL = reverse_lazy('login') 142 | LOGIN_REDIRECT_URL = reverse_lazy('home') 143 | 144 | 145 | # Phone number settings 146 | # https://github.com/stefanfoulis/django-phonenumber-field 147 | 148 | PHONENUMBER_DB_FORMAT = 'E164' 149 | PHONENUMBER_DEFAULT_REGION = 'US' 150 | -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /gardenhub/static/images/sunrise.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | sunrise 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | sunrise 27 | 29 | 30 | 31 | arif fajar yulianto 32 | 33 | 34 | https://thenounproject.com/term/sunrise/1473069 35 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 49 | 50 | 51 | 53 | 57 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /gardenhub/managers.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from django.db import models 3 | from django.db.models import Q 4 | from django.contrib.auth import get_user_model 5 | from django.contrib.auth.base_user import BaseUserManager 6 | from django.template.loader import render_to_string 7 | from gardenhub.utils import today 8 | 9 | 10 | class OrderQuerySet(models.QuerySet): 11 | """ 12 | Custom QuerySet for advanced filtering of orders. 13 | """ 14 | def open(self): 15 | """ Orders that have not finished but also may not have begun. """ 16 | return self.filter(end_date__gt=today()).exclude(canceled=True) 17 | 18 | def closed(self): 19 | """ Orders that have finished or were canceled. """ 20 | completed = self.filter(end_date__lt=today()) 21 | canceled = self.filter(canceled=True) 22 | return completed.union(canceled).distinct() 23 | 24 | def upcoming(self): 25 | """ Orders that have not yet begun but are scheduled to happen. """ 26 | return self.filter(start_date__gt=today()).exclude(canceled=True) 27 | 28 | def active(self): 29 | """ Orders that are happening right now. """ 30 | return self.filter( 31 | Q(end_date__gte=today()) & 32 | Q(start_date__lte=today()) 33 | ).exclude(canceled=True) 34 | 35 | def inactive(self): 36 | """ All orders that aren't happening right now. """ 37 | return self.filter( 38 | Q(end_date__lt=today()) | 39 | Q(start_date__gt=today()) | 40 | Q(canceled=True) 41 | ) 42 | 43 | def picked_today(self): 44 | """ Orders that have at least one Pick from today. """ 45 | return self.filter(plot__picks__timestamp__gte=today()) 46 | 47 | def unpicked_today(self): 48 | """ Orders that have no Picks from today. """ 49 | return self.exclude(plot__picks__timestamp__gte=today()) 50 | 51 | 52 | class UserManager(BaseUserManager): 53 | """ 54 | Custom User manager because the custom User model only does auth by email. 55 | """ 56 | use_in_migrations = True 57 | 58 | def _create_user(self, email, password, **extra_fields): 59 | """ 60 | Create and save a user with the given email and password. 61 | """ 62 | if not email: 63 | raise ValueError('The given email must be set') 64 | email = self.normalize_email(email) 65 | user = self.model(email=email, **extra_fields) 66 | user.set_password(password) 67 | user.save(using=self._db) 68 | return user 69 | 70 | def create_user(self, email=None, password=None, **extra_fields): 71 | extra_fields.setdefault('is_staff', False) 72 | extra_fields.setdefault('is_superuser', False) 73 | return self._create_user(email, password, **extra_fields) 74 | 75 | def create_superuser(self, email, password, **extra_fields): 76 | extra_fields.setdefault('is_active', True) 77 | extra_fields.setdefault('is_staff', True) 78 | extra_fields.setdefault('is_superuser', True) 79 | 80 | if extra_fields.get('is_staff') is not True: 81 | raise ValueError('Superuser must have is_staff=True.') 82 | if extra_fields.get('is_superuser') is not True: 83 | raise ValueError('Superuser must have is_superuser=True.') 84 | 85 | return self._create_user(email, password, **extra_fields) 86 | 87 | def get_or_invite_users(self, emails, request): 88 | """ 89 | Gets or creates users from the list of emails. When a user is created 90 | they are sent an invitation email. 91 | """ 92 | users = [] 93 | 94 | # Loop through the list of emails 95 | for email in emails: 96 | # Get or create a user from the email 97 | user, created = get_user_model().objects.get_or_create(email=email) 98 | # If the user was just newly created... 99 | if created: 100 | # Generate a random token for account activation 101 | user.activation_token = str(uuid.uuid4()) 102 | user.save() 103 | # Send the user an invitation email with their activation token 104 | inviter = request.user 105 | activate_url = request.build_absolute_uri( 106 | '/activate/{}/'.format(user.activation_token) 107 | ) 108 | email_data = { 109 | 'inviter': inviter, 110 | 'activate_url': activate_url 111 | } 112 | user.email_user( 113 | subject="{} invited you to join GardenHub".format( 114 | inviter.get_full_name()), 115 | message=render_to_string( 116 | 'gardenhub/email_invitation.txt', email_data 117 | ), 118 | html_message=render_to_string( 119 | 'gardenhub/email_invitation.html', email_data 120 | ) 121 | ) 122 | 123 | users.append(user) 124 | 125 | return users 126 | -------------------------------------------------------------------------------- /gardenhub/admin.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib import admin 3 | from django.contrib.auth.admin import UserAdmin as BaseUserAdmin 4 | from django.contrib.auth.forms import ReadOnlyPasswordHashField 5 | from .models import Crop, Garden, Plot, Pick, Order, Affiliation, User 6 | 7 | 8 | class PlotAdmin(admin.ModelAdmin): 9 | list_display = ('get_plot', 'get_garden') 10 | 11 | def get_plot(self, obj): 12 | return "Plot #{}".format(obj.title) 13 | get_plot.admin_order_field = 'title' 14 | get_plot.short_description = 'Plot' 15 | 16 | def get_garden(self, obj): 17 | return obj.garden 18 | get_garden.admin_order_field = 'garden__title' 19 | get_garden.short_description = 'Garden' 20 | 21 | 22 | class GardenAdmin(admin.ModelAdmin): 23 | list_display = ('title', 'address') 24 | 25 | 26 | class OrderAdmin(admin.ModelAdmin): 27 | readonly_fields = ('timestamp',) 28 | list_display = ( 29 | 'get_order', 'requester', 'start_date', 'end_date', 'get_plot', 30 | 'get_garden', 'timestamp' 31 | ) 32 | 33 | def get_order(self, obj): 34 | return "Order #{}".format(obj.id) 35 | get_order.admin_order_field = 'id' 36 | get_order.short_description = 'Order' 37 | 38 | def get_plot(self, obj): 39 | return "Plot #{}".format(obj.plot.title) 40 | get_plot.admin_order_field = 'plot__title' 41 | get_plot.short_description = 'Plot' 42 | 43 | def get_garden(self, obj): 44 | return obj.plot.garden 45 | get_garden.admin_order_field = 'plot__garden__title' 46 | get_garden.short_description = 'Garden' 47 | 48 | 49 | class PickAdmin(admin.ModelAdmin): 50 | readonly_fields = ('timestamp',) 51 | list_display = ('timestamp', 'picker', 'get_plot', 'get_garden') 52 | 53 | def get_plot(self, obj): 54 | return obj.plot.title 55 | get_plot.admin_order_field = 'plot__title' 56 | get_plot.short_description = 'Plot' 57 | 58 | def get_garden(self, obj): 59 | return obj.plot.garden 60 | get_garden.admin_order_field = 'plot__garden__title' 61 | get_garden.short_description = 'Garden' 62 | 63 | 64 | class UserCreationForm(forms.ModelForm): 65 | """A form for creating new users. Includes all the required 66 | fields, plus a repeated password.""" 67 | password1 = forms.CharField(label='Password', widget=forms.PasswordInput) 68 | password2 = forms.CharField( 69 | label='Password confirmation', widget=forms.PasswordInput) 70 | 71 | class Meta: 72 | model = User 73 | fields = ['email', 'first_name', 'last_name', 74 | 'is_active', 'is_staff', 'is_superuser'] 75 | 76 | def clean_password2(self): 77 | # Check that the two password entries match 78 | password1 = self.cleaned_data.get("password1") 79 | password2 = self.cleaned_data.get("password2") 80 | if password1 and password2 and password1 != password2: 81 | raise forms.ValidationError("Passwords don't match") 82 | return password2 83 | 84 | def save(self, commit=True): 85 | # Save the provided password in hashed format 86 | user = super().save(commit=False) 87 | user.set_password(self.cleaned_data["password1"]) 88 | if commit: 89 | user.save() 90 | return user 91 | 92 | 93 | class UserChangeForm(forms.ModelForm): 94 | """A form for updating users. Includes all the fields on 95 | the user, but replaces the password field with admin's 96 | password hash display field. 97 | """ 98 | password = ReadOnlyPasswordHashField() 99 | 100 | class Meta: 101 | model = User 102 | fields = ['email', 'phone_number', 'password', 'first_name', 103 | 'last_name', 'is_active', 'is_staff', 'is_superuser'] 104 | 105 | def clean_password(self): 106 | # Regardless of what the user provides, return the initial value. 107 | # This is done here, rather than on the field, because the 108 | # field does not have access to the initial value 109 | return self.initial["password"] 110 | 111 | 112 | class UserAdmin(BaseUserAdmin): 113 | # The forms to add and change user instances 114 | form = UserChangeForm 115 | add_form = UserCreationForm 116 | 117 | # The fields to be used in displaying the User model. 118 | # These override the definitions on the base UserAdmin 119 | # that reference specific fields on auth.User. 120 | list_display = ( 121 | 'email', 'first_name', 'last_name', 'is_active', 'is_superuser') 122 | fieldsets = ( 123 | (None, {'fields': ('email', 'phone_number', 'password')}), 124 | ('Personal', {'fields': ('first_name', 'last_name', 'photo')}), 125 | ('Status', {'fields': ('is_active', 'is_staff', 'is_superuser')}) 126 | ) 127 | # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin 128 | # overrides get_fieldsets to use this attribute when creating a user. 129 | add_fieldsets = ( 130 | (None, { 131 | 'classes': ('wide',), 132 | 'fields': ('email', 'password1', 'password2', 'first_name', 133 | 'last_name', 'is_active', 'is_staff', 'is_superuser')} 134 | ), 135 | ) 136 | search_fields = ('email', 'first_name', 'last_name') 137 | ordering = ('email', 'first_name', 'last_name') 138 | filter_horizontal = () 139 | 140 | 141 | admin.site.register(Crop) 142 | admin.site.register(Garden, GardenAdmin) 143 | admin.site.register(Plot, PlotAdmin) 144 | admin.site.register(Order, OrderAdmin) 145 | admin.site.register(Affiliation) 146 | admin.site.register(Pick, PickAdmin) 147 | admin.site.register(User, UserAdmin) 148 | -------------------------------------------------------------------------------- /gardenhub/test_factories.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from factory import Faker 3 | from gardenhub.factories import ( 4 | CropFactory, 5 | GardenFactory, 6 | PlotFactory, 7 | OrderFactory, 8 | PickFactory, 9 | ActiveUserFactory, 10 | GardenerFactory, 11 | GardenManagerFactory, 12 | PickerFactory 13 | ) 14 | 15 | 16 | class CropFactoryTestCase(TestCase): 17 | def test_create(self): 18 | """ Test creation """ 19 | self.assertTrue(CropFactory()) 20 | 21 | 22 | class GardenFactoryTestCase(TestCase): 23 | def test_create(self): 24 | """ Test creation """ 25 | self.assertTrue(GardenFactory()) 26 | 27 | def test_set_managers(self): 28 | """ Test setting managers """ 29 | managers = [ActiveUserFactory()] 30 | garden = GardenFactory(managers=managers) 31 | self.assertEqual(set(managers), set(garden.managers.all())) 32 | 33 | def test_set_pickers(self): 34 | """ Test setting pickers """ 35 | pickers = [ActiveUserFactory()] 36 | garden = GardenFactory(pickers=pickers) 37 | self.assertEqual(set(pickers), set(garden.pickers.all())) 38 | 39 | 40 | class PlotFactoryTestCase(TestCase): 41 | def test_create(self): 42 | """ Test creation """ 43 | self.assertTrue(PlotFactory()) 44 | 45 | def test_set_crops(self): 46 | """ Test setting crops """ 47 | crops = CropFactory.create_batch(5) 48 | plot = PlotFactory(crops=crops) 49 | self.assertEqual(set(crops), set(plot.crops.all())) 50 | 51 | def test_zero_default(self): 52 | """ By default, no crops are generated """ 53 | self.assertEqual(PlotFactory().crops.count(), 0) 54 | 55 | def test_no_crops(self): 56 | """ Make sure we can set no crops """ 57 | plot = PlotFactory(crops=[]) 58 | self.assertEqual(plot.crops.count(), 0) 59 | 60 | def test_crop_count(self): 61 | """ Passing crop_count generates the given number of crops """ 62 | plot = PlotFactory(crop_count=5) 63 | self.assertEqual(plot.crops.count(), 5) 64 | 65 | def test_set_gardeners(self): 66 | """ Test setting gardeners """ 67 | gardeners = [ActiveUserFactory()] 68 | plot = PlotFactory(gardeners=gardeners) 69 | self.assertEqual(set(gardeners), set(plot.gardeners.all())) 70 | 71 | 72 | class PickFactoryTestCase(TestCase): 73 | def test_create(self): 74 | """ Test creation """ 75 | self.assertTrue(PickFactory()) 76 | 77 | 78 | class OrderFactoryTestCase(TestCase): 79 | def test_create(self): 80 | """ Test creation """ 81 | order = OrderFactory( 82 | start_date=Faker("past_date"), end_date=Faker("future_date") 83 | ) 84 | self.assertTrue(order) 85 | 86 | 87 | class ActiveUserFactoryTestCase(TestCase): 88 | def test_create(self): 89 | """ Test creation """ 90 | self.assertTrue(ActiveUserFactory()) 91 | 92 | def test_is_active(self): 93 | """ Ensure the user is active """ 94 | self.assertTrue(ActiveUserFactory().is_active) 95 | 96 | 97 | class GardenerFactoryTestCase(TestCase): 98 | def test_create(self): 99 | """ Test creation """ 100 | self.assertTrue(GardenerFactory()) 101 | 102 | def test_is_gardener(self): 103 | """ The generated user should be a gardener """ 104 | self.assertTrue(GardenerFactory().is_gardener()) 105 | 106 | def test_set_plots(self): 107 | """ Make sure we can set plots """ 108 | plots = PlotFactory.create_batch(5) 109 | gardener = GardenerFactory(plots=plots) 110 | for plot in plots: 111 | self.assertIn(gardener, plot.gardeners.all()) 112 | 113 | def test_no_plots(self): 114 | """ Make sure we can set no plots """ 115 | gardener = GardenerFactory(plots=[]) 116 | self.assertTrue(gardener.get_plots().count(), 0) 117 | 118 | def test_gardener_only(self): 119 | """ By default, they're not a GM or Picker """ 120 | self.assertFalse(GardenerFactory().is_garden_manager()) 121 | self.assertFalse(GardenerFactory().is_picker()) 122 | 123 | 124 | class GardenManagerTestCase(TestCase): 125 | def test_create(self): 126 | """ Test creation """ 127 | self.assertTrue(GardenManagerFactory()) 128 | 129 | def test_is_garden_manager(self): 130 | """ The generated user should be a GM """ 131 | self.assertTrue(GardenManagerFactory().is_garden_manager()) 132 | 133 | def test_set_gardens(self): 134 | """ Make sure we can set gardens """ 135 | gardens = GardenFactory.create_batch(5) 136 | manager = GardenManagerFactory(gardens=gardens) 137 | for garden in gardens: 138 | self.assertIn(manager, garden.managers.all()) 139 | 140 | def test_no_gardens(self): 141 | """ Make sure we can set no gardens """ 142 | manager = GardenManagerFactory(gardens=[]) 143 | self.assertTrue(manager.get_gardens().count(), 0) 144 | 145 | def test_garden_manager_only(self): 146 | """ By default, they're not a Gardener or Picker """ 147 | self.assertFalse(GardenManagerFactory().is_gardener()) 148 | self.assertFalse(GardenManagerFactory().is_picker()) 149 | 150 | 151 | class PickerFactoryTestCase(TestCase): 152 | def test_create(self): 153 | """ Test creation """ 154 | self.assertTrue(PickerFactory()) 155 | 156 | def test_is_picker(self): 157 | """ The generated user should be a picker """ 158 | self.assertTrue(PickerFactory().is_picker()) 159 | 160 | def test_picker_only(self): 161 | """ By default, they're not a gardener or GM """ 162 | self.assertFalse(PickerFactory().is_gardener()) 163 | self.assertFalse(PickerFactory().is_garden_manager()) 164 | -------------------------------------------------------------------------------- /gardenhub/static/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 43 | 53 | 61 | 69 | 77 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/homepage.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | {% load static gardenhub %} 3 | 4 | {% block body_class %}home{% endblock %} 5 | 6 | {% block content %} 7 | {# Display active orders to the picker #} 8 | {% if request.user.is_picker %} 9 | 10 |

11 | {% now "l, N j" %} 12 | {{ request.user.first_name}}'s Assignments 13 | Let's go to work on these orders! 14 |

15 | {% for garden in request.user.get_picker_gardens %} 16 | {% with orders=request.user.get_picker_orders|picker_format:garden %} 17 |

18 | {{ garden.title }} 19 | {{ orders|length }} active order{{ orders|pluralize }} 20 |

21 | {% if orders %} 22 |
23 | {% for order in orders %} 24 |
25 |
26 | {% if order.was_picked_today %} 27 | 28 | {% else %} 29 | 30 | {% endif %} 31 |
Plot {{ order.plot.title }}
32 |
33 |
34 |

Order #{{ order.id }}

35 |

Crops to be Picked

36 |
37 | {% for crop in order.crops.all %} 38 |
39 | 40 |
41 |
{{ crop.title }}
42 |
43 |
44 | {% endfor %} 45 |
46 |
47 | 57 |
58 | {% endfor %} 59 |
60 | {% else %} 61 |

No orders for this garden!

62 | {% endif %} 63 | {% endwith %} 64 | {% endfor %} 65 | 66 | {% else %} 67 |
68 | {# Welcome the user with a nice message #} 69 |

70 |
71 | Let's get started. 72 |
Choose one of the options below to get begin.
73 |
74 |

75 | 76 | {# The user has the ability to create an order #} 77 | {% if request.user.is_gardener %} 78 |
79 |
80 | 81 |
82 | 83 | 84 | 85 | 86 | 87 |
88 | New Order 89 |
Create a brand new order.
90 |
91 |
92 | 93 | {% if request.user.has_orders %} 94 |
95 | 96 | 97 | 98 | 99 | 100 | 110 |
111 | {% endif %} 112 | 113 |
114 | 115 | 116 | 117 | 118 | 119 |
120 | 121 | {% if request.user.is_garden_manager %} 122 | Manage Plots 123 | {% else %} 124 | View My Plots 125 | {% endif %} 126 | 127 |
128 | {% if request.user.is_garden_manager %} 129 | Create and edit plots in your garden. 130 | {% else %} 131 | Edit your crops. 132 | {% endif %} 133 |
134 |
135 |
136 | 137 |
138 |
139 | 140 | {# The user cannot create an order #} 141 | {% else %} 142 |
143 |
You're not assigned to any plots :(
144 |

Normally you would have the ability to place an order, except you haven't been assigned to any plots yet. Try getting in touch with your garden manager and ask if they can add you to your plot.

145 |
146 | {% endif %} 147 |
148 | {% endif %} 149 | {% endblock %} 150 | 151 | {% block extra_scripts %} 152 | 157 | {% endblock %} 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GardenHub Promo Banner](https://raw.githubusercontent.com/HarvestHub/GardenHub/master/gardenhub-promo.png) 2 | 3 | [![Build Status](https://travis-ci.org/HarvestHub/GardenHub.svg?branch=master)](https://travis-ci.org/HarvestHub/GardenHub) 4 | [![Coverage Status](https://coveralls.io/repos/github/HarvestHub/GardenHub/badge.svg?branch=master)](https://coveralls.io/github/HarvestHub/GardenHub?branch=master) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/831094bb6605cfd9ec68/maintainability)](https://codeclimate.com/github/HarvestHub/GardenHub/maintainability) 6 | [![Known Vulnerabilities](https://snyk.io/test/github/harvesthub/gardenhub/badge.svg)](https://snyk.io/test/github/harvesthub/gardenhub) 7 | [![Requirements Status](https://requires.io/github/HarvestHub/GardenHub/requirements.svg?branch=master)](https://requires.io/github/HarvestHub/GardenHub/requirements/?branch=master) 8 | [![Documentation Status](https://readthedocs.org/projects/gardenhub/badge/?version=latest)](http://gardenhub.readthedocs.io/en/latest/?badge=latest) 9 | [![Docker Automated build](https://img.shields.io/docker/automated/harvesthub/gardenhub.svg)](https://hub.docker.com/r/harvesthub/gardenhub/) 10 | [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) 11 | [![Matrix ID](https://img.shields.io/badge/matrix-%23gardenhub%3Amatrix.org-brightgreen.svg)](https://riot.im/app/#/room/#gardenhub:matrix.org) 12 | 13 | # GardenHub 14 | 15 | Formed around the simple idea that food should not go to waste, GardenHub is the solution to the problem of community garden food waste. Despite the best efforts of community gardeners, far too often food produced in community gardens rots on the vine. 16 | 17 | GardenHub is building technology to enable gardeners to collaborate and act upon what's growing, ripening, and available for harvest in their gardens. Using this information, GardenHub notifies gardeners, local charities, restaurants, and other stakeholders of the availability of this food. 18 | 19 | [Read the full documentation on Read the Docs.](https://gardenhub.readthedocs.io/en/latest/) 20 | 21 | ## Local development 22 | 23 | GardenHub provides a script called `dev.sh` to make local development easy. As long as you have [Docker](https://www.docker.com/) installed, you do not need Python, Django, Postgres, or anything else running on your computer for local development with `dev.sh`. This is because `dev.sh` automatically configures a local development environment with Docker containers where all of that is already installed. 24 | 25 | **Note:** This has been tested on GNU/Linux. Your mileage may vary using Docker on other operating systems. 26 | 27 | You will need the following packages installed to develop on GardenHub: 28 | 29 | * docker-ce 30 | * docker-compose 31 | * git 32 | * bash 33 | 34 | ### Installing Docker 35 | 36 | To check if you already have Docker installed, run `docker -v` in your terminal. GardenHub has been tested with version 17. **If you already have it, skip to the next section.** 37 | 38 | [![asciicast](https://asciinema.org/a/158200.png)](https://asciinema.org/a/158200) 39 | 40 | On most GNU/Linux distros, you can use the following command to install Docker: 41 | 42 | ``` 43 | sudo sh -c "wget -nv -O - https://get.docker.com/ | sh" 44 | ``` 45 | 46 | After Docker is installed, you'll want to add your user to the docker group so you don't need `sudo` to run Docker commands. 47 | 48 | ``` 49 | sudo usermod -aG docker $(whoami) 50 | ``` 51 | 52 | Finally, log out and back into your computer, and then head to the next section. 53 | 54 | #### Installing Docker Compose 55 | 56 | You will also need [Docker Compose](https://docs.docker.com/compose/install/#install-compose) for local development. Check if you already have it by typing `docker-compose version`. 57 | 58 | If you need to install it, follow the link and select your platform for specific instructions. 59 | 60 | ### Starting the development server 61 | 62 | [![asciicast](https://asciinema.org/a/158203.png)](https://asciinema.org/a/158203) 63 | 64 | With Docker installed and ready to go, just follow these commands: 65 | 66 | ``` 67 | # Clone the repo 68 | git clone https://github.com/HarvestHub/GardenHub.git 69 | 70 | # Enter the project folder 71 | cd GardenHub 72 | 73 | # Run the local development server 74 | ./dev.sh start 75 | ``` 76 | 77 | It may take a few minutes to download everything the first time, then it will run more quickly on subsequent attempts. 78 | 79 | `dev.sh` has a few options you can take advantage of. 80 | 81 | | Command | Description | 82 | |-----------|-----------------------------------------------------------------------------------------------------------------------| 83 | | start | Launches a Postgres container and a GardenHub app container then starts `manage.py runserver`. | 84 | | build | Rebuilds the app container. You must do this **manually** if you change requirements.txt. | 85 | | manage.py | Same as running `python manage.py` in the app container. Useful for running migrations and other management commands. | 86 | | setup | Installs Docker. Works on many GNU/Linux distros. | 87 | | docs | Runs a local server for editing the docs. Uses sphinx-autobuild within the container. | 88 | 89 | **Hint:** `./dev.sh start` will run migrations every time before it starts the development server. To skip that, you can run `./dev.sh manage.py runserver` to run the development server directly. 90 | 91 | ### Running migrations and management commands 92 | 93 | To run migrations, you can use: 94 | 95 | ``` 96 | # Migrate 97 | ./dev.sh manage.py migrate 98 | 99 | # Make migrations 100 | ./dev.sh manage.py makemigrations 101 | ``` 102 | 103 | Any other management command may also be run this way. 104 | 105 | ### Rebuilding the container (you have to, sometimes) 106 | 107 | Changing application code shouldn't require rebuilding the container. However, you must **manually rebuild the container** any time you edit `requirements.txt`. This is because the requirements get installed into the container at build time. You can rebuild the container like so: 108 | 109 | ``` 110 | ./dev.sh build 111 | ``` 112 | 113 | ## License 114 | 115 | GardenHub is copyright © 2017 HarvestHub. Except where otherwise noted, the code in this repo is licensed under the GNU AGPL version 3 or later. View the `LICENSE` file for a copy of the full license. 116 | 117 | The documentation (in the `docs` subdirectory) is licensed under the GNU FDL version 1.3 or greater. View `docs/LICENSE` for a copy of the full license. 118 | 119 | Finally, the file `gardenhub/templates/gardenhub/email_invitation.html` is copyright Lee Munroe and licensed under MIT. The copyright notice and full license text is inside of the source code of the file. 120 | -------------------------------------------------------------------------------- /gardenhub/templates/gardenhub/order_form.html: -------------------------------------------------------------------------------- 1 | {% extends "gardenhub/_manage_base.html" %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 | {% csrf_token %} 7 | {% if form.non_field_errors %} 8 |
{{ form.non_field_errors }}
9 | {% endif %} 10 |
11 | 12 |

Select the plot you'd like picked.

13 | 26 | {{ form.plot.errors }} 27 |
28 |
29 |
30 | 31 |

When we should start picking.

32 |
33 |
34 | 35 | 36 |
37 |
38 | {{ form.start_date.errors }} 39 |
40 |
41 | 42 |

When we should stop picking.

43 |
44 |
45 | 46 | 47 |
48 |
49 | {{ form.end_date.errors }} 50 |
51 |
52 |
53 | 54 |
55 | 56 | 57 |
58 |
59 |
60 | 61 |

Select the crops you'd like picked. Tap the icons to select them.

62 | 63 |

You must select a plot before you can choose any crops.

64 | 75 | {{ form.crops.errors }} 76 |
77 |
78 | 79 |

Special instructions or comments about the order.

80 | 81 |
82 | 86 |
87 | {% endblock %} 88 | 89 | {% block extra_scripts %} 90 | 210 | {% endblock %} 211 | --------------------------------------------------------------------------------