├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── code_of_conduct.md ├── python-in-edu ├── manage.py ├── mysite │ ├── __init__.py │ ├── asgi.py │ ├── auth │ │ └── backends.py │ ├── settings.py │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── resources │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── choices.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20210225_2141.py │ │ ├── 0003_resource_license.py │ │ ├── 0004_auto_20210301_2251.py │ │ ├── 0005_auto_20210301_2304.py │ │ ├── 0006_resource_status.py │ │ ├── 0007_auto_20210511_1410.py │ │ ├── 0008_auto_20210512_1226.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ ├── django_registration │ │ │ ├── activation_complete.html │ │ │ ├── activation_email_body.txt │ │ │ ├── activation_email_subject.txt │ │ │ ├── activation_failed.html │ │ │ ├── registration_closed.html │ │ │ ├── registration_complete.html │ │ │ └── registration_form.html │ │ ├── misc │ │ │ ├── code_of_conduct.html │ │ │ ├── connect.html │ │ │ ├── getting_started.html │ │ │ └── index.html │ │ ├── registration │ │ │ ├── logged_out.html │ │ │ ├── login.html │ │ │ ├── password_reset_complete.html │ │ │ ├── password_reset_confirm.html │ │ │ ├── password_reset_done.html │ │ │ └── password_reset_form.html │ │ ├── resources │ │ │ ├── add_resource.html │ │ │ ├── base.html │ │ │ ├── field_include.html │ │ │ ├── full_width_base.html │ │ │ ├── profile_detail.html │ │ │ ├── profile_update.html │ │ │ ├── resource_detail.html │ │ │ ├── resource_form.html │ │ │ ├── resource_list.html │ │ │ └── update_resource.html │ │ └── text_includes │ │ │ ├── about_text.html │ │ │ ├── code_of_conduct_text.html │ │ │ ├── connect_text.html │ │ │ ├── contribute_text.html │ │ │ ├── getting_started_intro_text.html │ │ │ ├── resource_search_text.html │ │ │ ├── unpublished_resource_alert.html │ │ │ └── welcome.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── static_build │ └── emptyfile.txt └── static_source │ ├── css │ └── python.css │ ├── emptyfile.txt │ ├── guides │ ├── Python-Platform-Comparison.pdf │ ├── Take-Action-Toolkit.pdf │ └── what_works_toolkit.pdf │ └── images │ ├── Zen-of-OER.png │ ├── bg-head1.jpg │ ├── bg-planet.png │ ├── bg-stars.png │ ├── community.png │ ├── explore_with_python.png │ ├── favicon.ico │ ├── logo.png │ ├── separator1.png │ ├── separator2.png │ ├── separator3.png │ ├── separator4.png │ ├── snake_build.png │ ├── snake_learn.png │ └── snake_teach.png ├── requirements.txt └── runtime.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # IDE files and folders 132 | .idea/ 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 meg-ray 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: python ~/python-in-edu/manage.py migrate 2 | web: sh -c 'cd python-in-edu && gunicorn mysite.wsgi' 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Python in Education 2 | 3 | ## Join the Community 4 | 5 | We welcome contributions to this project! Our [issue tracker](https://github.com/psf/python-in-edu/issues) has open bugs, feature requests, etc. We encourage you to introduce yourself to the community [in our forums](https://discuss.python.org/c/education/31) before leaping into action. 6 | 7 | All contributors must agree to abide by our [Code of Conduct](https://github.com/psf/python-in-edu/blob/master/code_of_conduct.md). 8 | 9 | ## Installation Guide 10 | 11 | In order to run this site locally, you'll want to clone this repository and install the requirements (check the [Mac Troubleshooting](#mac-troubleshooting) section if you face any errors): 12 | 13 | ``` 14 | git clone https://github.com/psf/python-in-edu.git 15 | cd python-in-edu 16 | python3 -m venv .venv 17 | source .venv/bin/activate 18 | pip install -r requirements.txt 19 | ``` 20 | 21 | You can then change directories into the python-in-edu folder and build the database: 22 | 23 | ``` 24 | python manage.py migrate 25 | ``` 26 | 27 | 28 | To run the project locally, run the following command in the terminal: 29 | 30 | ``` 31 | python manage.py runserver 32 | ``` 33 | 34 | If you use [heroku cli installed on your system](https://devcenter.heroku.com/articles/heroku-local), simply run: 35 | 36 | ``` 37 | heroku local 38 | ``` 39 | 40 | To test, run: 41 | 42 | ``` 43 | python manage.py test 44 | ``` 45 | 46 | If you want to use or test email functionality locally, you'll need to [run a simple SMTP server](https://docs.djangoproject.com/en/3.1/topics/email/#configuring-email-for-development): 47 | 48 | python -m smtpd -n -c DebuggingServer localhost:1025 49 | 50 | 51 | # Mac Troubleshooting 52 | 53 | ### Postgres 54 | 55 | If you don't have an installation of Postgres on your system, you might run into the following error: 56 | 57 | ``` 58 | Error: pg_config executable not found. 59 | ``` 60 | 61 | [Install Postgres](https://postgresapp.com/) to resolve this issue. 62 | 63 | ### Pillow 64 | 65 | If your Pillow installation fails during installing the requirements with the following message: 66 | 67 | ``` 68 | The headers or library files could not be found for jpeg, 69 | a required dependency when compiling Pillow from source. 70 | ``` 71 | 72 | You can resolve this by installing [jpeg](https://formulae.brew.sh/formula/jpeg) using [homebrew](https://brew.sh/). 73 | -------------------------------------------------------------------------------- /code_of_conduct.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 127 | at [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /python-in-edu/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /python-in-edu/mysite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/python-in-edu/d4634d5357aea80ac260786bcdc3e1d13b042810/python-in-edu/mysite/__init__.py -------------------------------------------------------------------------------- /python-in-edu/mysite/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for mysite project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /python-in-edu/mysite/auth/backends.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.backends import ModelBackend 2 | from django.contrib.auth import get_user_model 3 | 4 | User = get_user_model() 5 | 6 | 7 | class _AuthBackend(ModelBackend): 8 | 9 | def get_user(self, user_id): 10 | try: 11 | return ( 12 | User._default_manager 13 | .get(pk=user_id)) 14 | except User.DoesNotExist: 15 | pass 16 | 17 | 18 | class EmailAuthBackend(_AuthBackend): 19 | 20 | def authenticate(self, request, username=None, password=None, **kwargs): 21 | if username is None: 22 | username = kwargs.get(User.USERNAME_FIELD) 23 | 24 | try: 25 | user = User._default_manager.get(email=username) 26 | except (User.DoesNotExist, User.MultipleObjectsReturned): 27 | User().set_password(password) 28 | else: 29 | if (user.check_password(password) and 30 | self.user_can_authenticate(user)): 31 | return user 32 | 33 | 34 | class UsernameAuthBackend(_AuthBackend): 35 | 36 | def authenticate(self, request, username=None, password=None, **kwargs): 37 | if username is None: 38 | username = kwargs.get(User.USERNAME_FIELD) 39 | return super(UsernameAuthBackend, self).authenticate( 40 | request, username=username, password=password, **kwargs) 41 | -------------------------------------------------------------------------------- /python-in-edu/mysite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for mysite project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | import os 15 | import dj_database_url 16 | 17 | 18 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 19 | BASE_DIR = Path(__file__).resolve().parent.parent 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | if 'DJANGO_SECRET_KEY' in os.environ: # running on heroku 26 | SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') 27 | else: 28 | SECRET_KEY = 'o9ytrr4zhd-m92qt7$mb@3c0bg55s29x#0dje%(w9e^xmy)h-m' 29 | 30 | # SECURITY WARNING: don't run with debug turned on in production! 31 | if os.getenv('DEBUG'): 32 | DEBUG = True 33 | 34 | ALLOWED_HOSTS = ['python-in-edu.herokuapp.com', '127.0.0.1', 'education.python.org'] 35 | 36 | 37 | # Application definition 38 | 39 | INSTALLED_APPS = [ 40 | 'django.contrib.auth', 41 | 'django.contrib.contenttypes', 42 | 'django.contrib.sessions', 43 | 'django.contrib.messages', 44 | 'django.contrib.staticfiles', 45 | 'django_registration', 46 | 'mysite', 47 | 'resources', 48 | 'django.contrib.admin', 49 | 'multiselectfield', 50 | ] 51 | 52 | MIDDLEWARE = [ 53 | 'django.middleware.security.SecurityMiddleware', 54 | 'django.contrib.sessions.middleware.SessionMiddleware', 55 | 'django.middleware.common.CommonMiddleware', 56 | 'django.middleware.csrf.CsrfViewMiddleware', 57 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 58 | 'django.contrib.messages.middleware.MessageMiddleware', 59 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 60 | 'whitenoise.middleware.WhiteNoiseMiddleware', 61 | ] 62 | 63 | ROOT_URLCONF = 'mysite.urls' 64 | 65 | TEMPLATES = [ 66 | { 67 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 68 | 'DIRS': [], 69 | 'APP_DIRS': True, 70 | 'OPTIONS': { 71 | 'context_processors': [ 72 | 'django.template.context_processors.debug', 73 | 'django.template.context_processors.request', 74 | 'django.contrib.auth.context_processors.auth', 75 | 'django.contrib.messages.context_processors.messages', 76 | ], 77 | }, 78 | }, 79 | ] 80 | 81 | AUTHENTICATION_BACKENDS = [ 82 | 'mysite.auth.backends.UsernameAuthBackend', 83 | 'mysite.auth.backends.EmailAuthBackend', 84 | ] 85 | 86 | WSGI_APPLICATION = 'mysite.wsgi.application' 87 | 88 | 89 | # Database 90 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 91 | 92 | 93 | if 'DATABASE_URL' in os.environ: # running on heroku 94 | DATABASES = {} 95 | DATABASES['default'] = dj_database_url.config(conn_max_age=600) 96 | else: # running locally 97 | DATABASES = { 98 | 'default': { 99 | 'ENGINE': 'django.db.backends.sqlite3', 100 | 'NAME': BASE_DIR / 'db.sqlite3', 101 | } 102 | } 103 | 104 | if 'FORCE_HTTPS' in os.environ: # running on heroku 105 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 106 | SECURE_SSL_REDIRECT = True 107 | 108 | # Password validation 109 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 110 | 111 | AUTH_PASSWORD_VALIDATORS = [ 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 114 | }, 115 | { 116 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 117 | }, 118 | { 119 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 120 | }, 121 | { 122 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 123 | }, 124 | ] 125 | 126 | 127 | # Internationalization 128 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 129 | 130 | LANGUAGE_CODE = 'en-us' 131 | 132 | TIME_ZONE = 'UTC' 133 | 134 | USE_I18N = True 135 | 136 | USE_L10N = True 137 | 138 | USE_TZ = True 139 | 140 | 141 | # Static files (CSS, JavaScript, Images) 142 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 143 | STATIC_ROOT = os.path.join(BASE_DIR, 'static_build') 144 | STATIC_URL = '/static/' 145 | 146 | # Extra places for collectstatic to find static files. 147 | STATICFILES_DIRS = ( 148 | os.path.join(BASE_DIR, 'static_source'), 149 | ) 150 | 151 | 152 | # Django Registration 153 | ACCOUNT_ACTIVATION_DAYS = 7 # One-week activation window 154 | LOGIN_REDIRECT_URL = "/" 155 | LOGIN_URL = "login" 156 | EMAIL_PORT = 1025 157 | 158 | 159 | if 'EMAIL_HOST' in os.environ: # running on heroku, probably 160 | EMAIL_HOST = os.environ.get('EMAIL_HOST') 161 | EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') 162 | EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') 163 | EMAIL_PORT = int(os.environ.get('EMAIL_PORT')) 164 | EMAIL_USE_TLS = True 165 | DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL') 166 | else: 167 | DEFAULT_FROM_EMAIL = "example@example.com" 168 | 169 | SEND_MAIL = 'PY_IN_EDU_DONT_SEND_MAIL' not in os.environ 170 | -------------------------------------------------------------------------------- /python-in-edu/mysite/urls.py: -------------------------------------------------------------------------------- 1 | """mysite URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin, staticfiles 17 | from django.contrib.staticfiles.storage import staticfiles_storage 18 | from django.urls import include, path 19 | from django.views.generic import RedirectView 20 | 21 | from .views import IndexView 22 | 23 | urlpatterns = [ 24 | path('', IndexView.as_view(), name='index'), 25 | path('accounts/', include('django_registration.backends.activation.urls')), 26 | path('accounts/', include(('django.contrib.auth.urls'))), 27 | path('resources/', include('resources.urls')), 28 | path('admin/', admin.site.urls), 29 | path('forum/', RedirectView.as_view(url="https://discuss.python.org/c/education/31")), 30 | path('favicon.ico', RedirectView.as_view(url=staticfiles_storage.url('favicon.ico'))) 31 | ] 32 | -------------------------------------------------------------------------------- /python-in-edu/mysite/views.py: -------------------------------------------------------------------------------- 1 | from django.views import generic 2 | 3 | class IndexView(generic.TemplateView): 4 | template_name = 'misc/index.html' 5 | -------------------------------------------------------------------------------- /python-in-edu/mysite/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for mysite project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/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', 'mysite.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /python-in-edu/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/python-in-edu/d4634d5357aea80ac260786bcdc3e1d13b042810/python-in-edu/resources/__init__.py -------------------------------------------------------------------------------- /python-in-edu/resources/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Profile, Resource 4 | 5 | 6 | admin.site.register(Profile) 7 | admin.site.register(Resource) -------------------------------------------------------------------------------- /python-in-edu/resources/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ResourcesConfig(AppConfig): 5 | name = 'resources' 6 | -------------------------------------------------------------------------------- /python-in-edu/resources/choices.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext_lazy as _ 2 | from django.db import models 3 | 4 | 5 | class ResourceStatusChoices(models.TextChoices): 6 | PROPOSED = 'PR', _('Proposed') 7 | ACCEPTED = 'AC', _('Accepted') 8 | REJECTED = 'RJ', _('Rejected') 9 | WITHDRAWN = 'WD', _('Withdrawn') 10 | 11 | 12 | class ResourceTypeChoices(models.TextChoices): 13 | PLATFORM_APP = 'PA', _('Platform or App') 14 | CURRICULUM = 'CU', _('Curriculum') 15 | TUTORIAL_COURSE = 'TC', _('Tutorial or Course') 16 | BOOK = 'BK', _('Book') 17 | WORKED_EXAMPLE = 'WE', _('Worked Example') 18 | DOCUMENTATION = 'DC', _('Documentation') 19 | OTHER = 'OT', _('Other') 20 | 21 | 22 | class AudienceChoices(models.TextChoices): 23 | K_THROUGH_12 = 'K12', _('K-12') 24 | HIGHER_ED = 'HIE', _('Higher Education') 25 | PROFESSIONAL_TRAINING = 'PFT', _('Professional Training') 26 | NOT_SPECIFIC = 'NSP', _('Not Specific') 27 | OTHER = 'OTH', _('Other') 28 | 29 | 30 | class DeviceChoices(models.TextChoices): 31 | DESKTOP_OR = 'DOL', _('Desktop or Laptop Computer') 32 | CHROMEBOOK_OR = 'CON', _('Chromebook or Other Netbook') 33 | IPAD = 'IPD', _('iPad') 34 | ANDROID_TABLET = 'ATB', _('Android Tablet') 35 | IPHONE = 'IPH', _('iPhone') 36 | ANDROID_PHONE = 'APH', _('Android Phone') 37 | RASPBERRY_PI = 'RSP', _('Raspberry Pi') 38 | MICROCONTROLLERS = 'MCC', _('Microcontroller(s)') 39 | OTHER = 'OTH', _('Other') 40 | 41 | 42 | class UserRoleChoices(models.TextChoices): 43 | STUDENT = 'STD', _('Student') 44 | PS_EDUCATOR_SCHOOL = 'PES', _('Primary/Secondary Educator (school setting)') 45 | PS_EDUCATOR_OO_SCHOOL = 'PEO', _('Primary/Secondary Educator (out of school setting)') 46 | TEACHING_FACULTY = 'TF', _('Teaching Faculty (post-secondary)') 47 | ADULT_EDU_BOOTCAMP_ETC = 'AEB', _('Adult Educator or Trainer (bootcamp, industry)') 48 | ADULT_EDU_COACHING_ETC = 'AEC', _('Adult Educator or Trainer (teacher PD, coaching)') 49 | CURRICULUM_DEVELOPER = 'CUR', _('Curriculum or Product Developer') 50 | EDUCATION_VOLUNTEER = 'VOL', _('Education Volunteer') 51 | RESEARCHER = 'RES', _('Researcher') 52 | MENTOR = 'MNT', _('Mentor') 53 | INDUSTRY_PROF = 'INP', _('Industry Professional (Tech/Software/CS)') 54 | EDUCATION_DEVELOPER = 'EDV', _('Educational program developer') 55 | PARENT = 'PRT', _('Parent supporting education') 56 | OTHER = 'OTH', _('Other') 57 | 58 | 59 | class PopulationChoices(models.TextChoices): 60 | PRIMARY = 'PRI', _('Primary') 61 | SECONDAY = 'SEC', _('Secondary ') 62 | COLLEGE = 'COL', _('College/University') 63 | ADULT = 'ADU', _('Adult/Professional') 64 | OTHER = 'OTH', _('Other') 65 | NONE = 'NON', _('None') 66 | 67 | 68 | class UseTypeChoices(models.TextChoices): 69 | OPEN_SOURCE_PROJECT = 'OSP', _('Open Source Project - accepts contributions') 70 | OPEN_EDUCATION_RESOURCE = 'OER', _('Open Education Resource - ok to distribute and/or revise/remix') 71 | FREE_RESOURCE = 'FRE', _('Free Resource - free to use') 72 | FREEMIUM = 'IUM', _('Freemium - significant portion of resource free to use') 73 | PAID = 'PAI', _('Paid - costs money to access this resource') 74 | UNKOWN = 'UNK', _('Bleh') 75 | 76 | class PythonChoices(models.TextChoices): 77 | PYTHON_SPECIFIC = 'PS', _('Python Specific - part or all of resource is Python specific') 78 | LANGUAGE_AGNOSTIC = 'LA', _('Language Agnostic - can be used with any programming language') 79 | UNKNOWN = 'UN',_('Unkown') 80 | 81 | class SignUpChoices(models.TextChoices): 82 | CREATE_ACCOUNT = 'CA', _('Must create an account') 83 | PROVIDE_EMAIL = 'PE', _('Must provide email address') 84 | NO_REQUIREMENT = 'NR',_('No sign up requirement') 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /python-in-edu/resources/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.6 on 2021-02-10 21:54 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Resource', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=200)), 22 | ('url', models.CharField(max_length=200)), 23 | ('submitter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 24 | ], 25 | ), 26 | migrations.CreateModel( 27 | name='Profile', 28 | fields=[ 29 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 30 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 31 | ], 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /python-in-edu/resources/migrations/0002_auto_20210225_2141.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.6 on 2021-02-25 21:41 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('resources', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='resource', 15 | name='attribution', 16 | field=models.CharField(blank=True, max_length=250, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='resource', 20 | name='audience', 21 | field=models.CharField(choices=[('K12', 'K-12'), ('HIE', 'Higher Education'), ('PFT', 'Professional Training'), ('NSP', 'Not Specific'), ('OTH', 'Other')], default='K12', max_length=3), 22 | preserve_default=False, 23 | ), 24 | migrations.AddField( 25 | model_name='resource', 26 | name='author_bio', 27 | field=models.CharField(blank=True, max_length=250, null=True), 28 | ), 29 | migrations.AddField( 30 | model_name='resource', 31 | name='contact', 32 | field=models.CharField(blank=True, max_length=250, null=True), 33 | ), 34 | migrations.AddField( 35 | model_name='resource', 36 | name='description', 37 | field=models.CharField(blank=True, max_length=250, null=True), 38 | ), 39 | migrations.AddField( 40 | model_name='resource', 41 | name='devices', 42 | field=models.CharField(choices=[('DOL', 'Desktop or Laptop Computer'), ('CON', 'Chromebook or Other Netbook'), ('IPD', 'iPad'), ('ATB', 'Android Tablet'), ('IPH', 'iPhone'), ('APH', 'Android Phone'), ('RSP', 'Raspberry Pi'), ('MCC', 'Microcontroller(s)'), ('OTH', 'Other')], default='OTH', max_length=3), 43 | preserve_default=False, 44 | ), 45 | migrations.AddField( 46 | model_name='resource', 47 | name='language', 48 | field=models.CharField(default='English', max_length=50), 49 | preserve_default=False, 50 | ), 51 | migrations.AddField( 52 | model_name='resource', 53 | name='organization', 54 | field=models.CharField(blank=True, max_length=250, null=True), 55 | ), 56 | migrations.AddField( 57 | model_name='resource', 58 | name='requirements', 59 | field=models.CharField(default='stuff', max_length=200), 60 | preserve_default=False, 61 | ), 62 | migrations.AddField( 63 | model_name='resource', 64 | name='requires_signup', 65 | field=models.BooleanField(default=False), 66 | ), 67 | migrations.AddField( 68 | model_name='resource', 69 | name='resource_type', 70 | field=models.CharField(choices=[('PA', 'Platform or App'), ('CU', 'Curriculum'), ('TC', 'Tutorial or Course'), ('BK', 'Book'), ('WE', 'Worked Example'), ('DC', 'Documentation'), ('OT', 'Other')], default='OT', max_length=2), 71 | preserve_default=False, 72 | ), 73 | migrations.AddField( 74 | model_name='resource', 75 | name='standards', 76 | field=models.CharField(blank=True, max_length=250, null=True), 77 | ), 78 | ] 79 | -------------------------------------------------------------------------------- /python-in-edu/resources/migrations/0003_resource_license.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.6 on 2021-03-01 21:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('resources', '0002_auto_20210225_2141'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='resource', 15 | name='license', 16 | field=models.CharField(default='MIT', max_length=200), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /python-in-edu/resources/migrations/0004_auto_20210301_2251.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.6 on 2021-03-01 22:51 2 | 3 | from django.db import migrations, models 4 | import multiselectfield.db.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('resources', '0003_resource_license'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='profile', 16 | name='country', 17 | field=models.CharField(blank=True, max_length=100, null=True), 18 | ), 19 | migrations.AddField( 20 | model_name='profile', 21 | name='organization', 22 | field=models.CharField(blank=True, max_length=100, null=True), 23 | ), 24 | migrations.AddField( 25 | model_name='profile', 26 | name='populations', 27 | field=multiselectfield.db.fields.MultiSelectField(choices=[('PRI', 'Primary'), ('SEC', 'Secondary '), ('COL', 'College/University'), ('ADU', 'Adult/Professional'), ('OTH', 'Other'), ('NON', 'None')], default='OTH', max_length=23), 28 | preserve_default=False, 29 | ), 30 | migrations.AddField( 31 | model_name='profile', 32 | name='psf_member', 33 | field=models.BooleanField(default=False), 34 | ), 35 | migrations.AddField( 36 | model_name='profile', 37 | name='roles', 38 | field=multiselectfield.db.fields.MultiSelectField(choices=[('STD', 'Student'), ('PES', 'Primary/Secondary Educator (school setting)'), ('PEO', 'Primary/Secondary Educator (out of school setting)'), ('TF', 'Teaching Faculty (post-secondary)'), ('AEB', 'Adult Educator or Trainer (bootcamp, industry)'), ('AEC', 'Adult Educator or Trainer (teacher PD, coaching)'), ('CUR', 'Curriculum or Product Developer'), ('VOL', 'Education Volunteer'), ('RES', 'Researcher'), ('MNT', 'Mentor'), ('INP', 'Industry Professional (Tech/Software/CS)'), ('EDV', 'Educational program developer'), ('PRT', 'Parent supporting education'), ('OTH', 'Other')], default='OTH', max_length=54), 39 | preserve_default=False, 40 | ), 41 | migrations.AddField( 42 | model_name='profile', 43 | name='work_with_underrepresented_groups', 44 | field=models.BooleanField(default=False), 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /python-in-edu/resources/migrations/0005_auto_20210301_2304.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.6 on 2021-03-01 23:04 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('resources', '0004_auto_20210301_2251'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='profile', 15 | old_name='work_with_underrepresented_groups', 16 | new_name='underrep', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /python-in-edu/resources/migrations/0006_resource_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.6 on 2021-03-09 16:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('resources', '0005_auto_20210301_2304'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='resource', 15 | name='status', 16 | field=models.CharField(choices=[('PR', 'Proposed'), ('AC', 'Accepted'), ('RJ', 'Rejected'), ('WD', 'Withdrawn')], default='PR', max_length=3), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /python-in-edu/resources/migrations/0007_auto_20210511_1410.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.10 on 2021-05-11 14:10 2 | 3 | from django.db import migrations, models 4 | import multiselectfield.db.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('resources', '0006_resource_status'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='resource', 16 | name='author_bio', 17 | ), 18 | migrations.RemoveField( 19 | model_name='resource', 20 | name='organization', 21 | ), 22 | migrations.RemoveField( 23 | model_name='resource', 24 | name='requirements', 25 | ), 26 | migrations.RemoveField( 27 | model_name='resource', 28 | name='standards', 29 | ), 30 | migrations.RemoveField( 31 | model_name='resource', 32 | name='url', 33 | ), 34 | migrations.AddField( 35 | model_name='resource', 36 | name='python_related', 37 | field=models.CharField(choices=[('PS', 'Python Specific - part or all of resource is Python specific'), ('LA', 'Language Agnostic - can be used with any programming language'), ('UN', 'Unkown')], default='UN', help_text='Select the option that best describes this resource.', max_length=2), 38 | ), 39 | migrations.AddField( 40 | model_name='resource', 41 | name='url1', 42 | field=models.CharField(default='bleh', help_text='You must link at least one resource.', max_length=200), 43 | preserve_default=False, 44 | ), 45 | migrations.AddField( 46 | model_name='resource', 47 | name='url2', 48 | field=models.CharField(blank=True, help_text='Optional additional url related to the same resource', max_length=200, null=True), 49 | ), 50 | migrations.AddField( 51 | model_name='resource', 52 | name='url3', 53 | field=models.CharField(blank=True, help_text='Optional additional url related to the same resource', max_length=200, null=True), 54 | ), 55 | migrations.AddField( 56 | model_name='resource', 57 | name='url_description1', 58 | field=models.CharField(blank=True, help_text='Use this field, if you are including multiple urls', max_length=50, null=True), 59 | ), 60 | migrations.AddField( 61 | model_name='resource', 62 | name='url_description2', 63 | field=models.CharField(blank=True, help_text='Use this field, if you are including multiple urls', max_length=50, null=True), 64 | ), 65 | migrations.AddField( 66 | model_name='resource', 67 | name='url_description3', 68 | field=models.CharField(blank=True, help_text='Use this field, if you are including multiple urls', max_length=50, null=True), 69 | ), 70 | migrations.AddField( 71 | model_name='resource', 72 | name='use_type', 73 | field=models.CharField(choices=[('OSP', 'Open Source Project - accepts contributions'), ('OER', 'Open Education Resource - ok to distribute and/or revise/remix'), ('FRE', 'Free Resource - free to use'), ('IUM', 'Freemium - significant portion of resource free to use'), ('PAI', 'Paid - costs money to access this resource'), ('UNK', 'Bleh')], default='UN', help_text='Select the use type that best describes this resource.', max_length=3), 74 | ), 75 | migrations.AlterField( 76 | model_name='resource', 77 | name='attribution', 78 | field=models.CharField(default='bleh', help_text='What person or organization created this resource?', max_length=250), 79 | preserve_default=False, 80 | ), 81 | migrations.AlterField( 82 | model_name='resource', 83 | name='audience', 84 | field=multiselectfield.db.fields.MultiSelectField(choices=[('K12', 'K-12'), ('HIE', 'Higher Education'), ('PFT', 'Professional Training'), ('NSP', 'Not Specific'), ('OTH', 'Other')], help_text="Select 'not specific' for resources for any or all audiences.", max_length=3), 85 | ), 86 | migrations.AlterField( 87 | model_name='resource', 88 | name='contact', 89 | field=models.CharField(blank=True, help_text='Not for display, What is the best way to reach you if we have questions about this submission?', max_length=250, null=True), 90 | ), 91 | migrations.AlterField( 92 | model_name='resource', 93 | name='description', 94 | field=models.CharField(default='bleh', help_text='Add a description of this resource. (max 500 characters)', max_length=500), 95 | preserve_default=False, 96 | ), 97 | migrations.AlterField( 98 | model_name='resource', 99 | name='devices', 100 | field=multiselectfield.db.fields.MultiSelectField(choices=[('DOL', 'Desktop or Laptop Computer'), ('CON', 'Chromebook or Other Netbook'), ('IPD', 'iPad'), ('ATB', 'Android Tablet'), ('IPH', 'iPhone'), ('APH', 'Android Phone'), ('RSP', 'Raspberry Pi'), ('MCC', 'Microcontroller(s)'), ('OTH', 'Other')], help_text='Which devices are compatible with this resource', max_length=3), 101 | ), 102 | migrations.AlterField( 103 | model_name='resource', 104 | name='language', 105 | field=models.CharField(blank=True, help_text='What language/s are the written materials available in?', max_length=50, null=True), 106 | ), 107 | migrations.AlterField( 108 | model_name='resource', 109 | name='license', 110 | field=models.CharField(blank=True, help_text="What is the copyright license type? Type 'unknown' if the license type is not available.", max_length=200, null=True), 111 | ), 112 | migrations.AlterField( 113 | model_name='resource', 114 | name='requires_signup', 115 | field=models.CharField(choices=[('CA', 'Must create an account'), ('PE', 'Must provide email address'), ('NR', 'No sign up requirement')], help_text='Are users required to create an account or provide their email address to access this resource?', max_length=3), 116 | ), 117 | migrations.AlterField( 118 | model_name='resource', 119 | name='resource_type', 120 | field=multiselectfield.db.fields.MultiSelectField(choices=[('PA', 'Platform or App'), ('CU', 'Curriculum'), ('TC', 'Tutorial or Course'), ('BK', 'Book'), ('WE', 'Worked Example'), ('DC', 'Documentation'), ('OT', 'Other')], help_text='Select all that apply.', max_length=3), 121 | ), 122 | migrations.AlterField( 123 | model_name='resource', 124 | name='title', 125 | field=models.CharField(help_text='What is the name of the resource', max_length=200), 126 | ), 127 | ] 128 | -------------------------------------------------------------------------------- /python-in-edu/resources/migrations/0008_auto_20210512_1226.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.10 on 2021-05-12 12:26 2 | 3 | from django.db import migrations 4 | import multiselectfield.db.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('resources', '0007_auto_20210511_1410'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='resource', 16 | name='audience', 17 | field=multiselectfield.db.fields.MultiSelectField(choices=[('K12', 'K-12'), ('HIE', 'Higher Education'), ('PFT', 'Professional Training'), ('NSP', 'Not Specific'), ('OTH', 'Other')], help_text="Select 'not specific' for resources for any or all audiences.", max_length=30), 18 | ), 19 | migrations.AlterField( 20 | model_name='resource', 21 | name='devices', 22 | field=multiselectfield.db.fields.MultiSelectField(choices=[('DOL', 'Desktop or Laptop Computer'), ('CON', 'Chromebook or Other Netbook'), ('IPD', 'iPad'), ('ATB', 'Android Tablet'), ('IPH', 'iPhone'), ('APH', 'Android Phone'), ('RSP', 'Raspberry Pi'), ('MCC', 'Microcontroller(s)'), ('OTH', 'Other')], help_text='Which devices are compatible with this resource', max_length=30), 23 | ), 24 | migrations.AlterField( 25 | model_name='resource', 26 | name='resource_type', 27 | field=multiselectfield.db.fields.MultiSelectField(choices=[('PA', 'Platform or App'), ('CU', 'Curriculum'), ('TC', 'Tutorial or Course'), ('BK', 'Book'), ('WE', 'Worked Example'), ('DC', 'Documentation'), ('OT', 'Other')], help_text='Select all that apply.', max_length=30), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /python-in-edu/resources/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psf/python-in-edu/d4634d5357aea80ac260786bcdc3e1d13b042810/python-in-edu/resources/migrations/__init__.py -------------------------------------------------------------------------------- /python-in-edu/resources/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models.signals import post_save 3 | from django.contrib.auth.models import User 4 | from django.core.mail import send_mail 5 | from django.urls import reverse 6 | 7 | from multiselectfield import MultiSelectField 8 | 9 | from mysite.settings import DEFAULT_FROM_EMAIL, SEND_MAIL 10 | from . import choices 11 | 12 | 13 | # Profile Models 14 | 15 | 16 | class Profile(models.Model): 17 | 18 | user = models.OneToOneField(User, on_delete=models.CASCADE) 19 | organization = models.CharField(max_length=100, blank=True, null=True) 20 | country = models.CharField(max_length=100, blank=True, null=True) 21 | roles = MultiSelectField(choices=choices.UserRoleChoices.choices) 22 | populations = MultiSelectField(choices=choices.PopulationChoices.choices) 23 | underrep = models.BooleanField(default=False) 24 | psf_member = models.BooleanField(default=False) 25 | 26 | def __str__(self): 27 | return f"{self.user.username}" 28 | 29 | 30 | def create_user_profile(sender, instance, created, **kwargs): 31 | if created: 32 | Profile.objects.create(user=instance) 33 | 34 | post_save.connect(create_user_profile, sender=User) 35 | 36 | 37 | # Resource Models 38 | 39 | #class Link(models.Model): 40 | 41 | 42 | 43 | class Resource(models.Model): 44 | #Required and optional fields 45 | url1 = models.CharField(max_length=200, help_text="You must link at least one resource.") 46 | url_description1 = models.CharField(max_length=50, blank=True, null=True, help_text="Use this field, if you are including multiple urls") 47 | # resource = models.ForeignKey('Resource', on_delete=models.CASCADE, related_name='links') 48 | url2 = models.CharField(max_length=200, blank=True, null=True, help_text="Optional additional url related to the same resource") 49 | url_description2 = models.CharField(max_length=50, blank=True, null=True, help_text="Use this field, if you are including multiple urls") 50 | # resource = models.ForeignKey('Resource', on_delete=models.CASCADE, related_name='links') 51 | url3 = models.CharField(max_length=200, blank=True, null=True, help_text="Optional additional url related to the same resource") 52 | url_description3 = models.CharField(max_length=50, blank=True, null=True, help_text="Use this field, if you are including multiple urls") 53 | # resource = models.ForeignKey('Resource', on_delete=models.CASCADE, related_name='links') 54 | 55 | # core fields 56 | title = models.CharField(max_length=200, help_text="What is the name of the resource") 57 | submitter = models.ForeignKey(User, on_delete=models.CASCADE) # FIXME: probably want to orphan rather than delete 58 | status = models.CharField(max_length=3, choices=choices.ResourceStatusChoices.choices, default=choices.ResourceStatusChoices.PROPOSED) 59 | 60 | # required fields 61 | requires_signup = models.CharField(max_length=3,choices=choices.SignUpChoices.choices, help_text="Are users required to create an account or provide their email address to access this resource?") 62 | resource_type = MultiSelectField(max_length=30,choices=choices.ResourceTypeChoices.choices, help_text="Select all that apply.") 63 | audience = MultiSelectField(max_length=30,choices=choices.AudienceChoices.choices, help_text="Select 'not specific' for resources for any or all audiences.") 64 | devices = MultiSelectField(max_length=30,choices=choices.DeviceChoices.choices, help_text="Which devices are compatible with this resource") 65 | description = models.CharField(max_length=500, help_text="Add a description of this resource. (max 500 characters)") 66 | attribution = models.CharField(max_length=250, help_text="What person or organization created this resource?") 67 | use_type = models.CharField(max_length=3, choices=choices.UseTypeChoices.choices, help_text="Select the use type that best describes this resource.", default=choices.PythonChoices.UNKNOWN) 68 | python_related = models.CharField(max_length=2, choices=choices.PythonChoices.choices, help_text="Select the option that best describes this resource.", default=choices.PythonChoices.UNKNOWN) 69 | 70 | # optional fields 71 | 72 | #author_bio = models.CharField(max_length=250, blank=True, null=True) 73 | #organization = models.CharField(max_length=250, blank=True, null=True) 74 | contact = models.CharField(max_length=250, blank=True, null=True, help_text="Not for display, What is the best way to reach you if we have questions about this submission?") 75 | #standards = models.CharField(max_length=250, blank=True, null=True) 76 | language = models.CharField(max_length=50, blank=True, null=True, help_text="What language/s are the written materials available in?") 77 | #requirements = models.CharField(max_length=200, blank=True, null=True) 78 | license = models.CharField(max_length=200, blank=True, null=True, help_text="What is the copyright license type? Type 'unknown' if the license type is not available.") 79 | 80 | def __str__(self): 81 | return f"{self.title} (submitted by {self.submitter}) - {self.get_status_display()}" 82 | 83 | 84 | def resource_updated(sender, instance, created, **kwargs): 85 | 86 | if created and SEND_MAIL: 87 | staff_emails = [user.email for user in User.objects.all() if user.is_staff and user.email] 88 | subj = "A new resource has been proposed on Python In Education" 89 | url = "http://education.python.org" + reverse('admin:resources_resource_change', args=[instance.pk]) 90 | msg = f"A new resource with title '{instance.title}' has been proposed. Visit to approve: {url}" 91 | send_mail(subj, msg, DEFAULT_FROM_EMAIL, staff_emails, fail_silently=False) 92 | 93 | 94 | post_save.connect(resource_updated, sender=Resource) -------------------------------------------------------------------------------- /python-in-edu/resources/templates/django_registration/activation_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "resources/base.html" %} 2 | 3 | {% block content %} 4 | 5 |
30 | Register an account 31 | Reset your password 32 |
33 | 34 |Your password has been set. You may go ahead and log in now.
6 | 7 | {% endblock content %} -------------------------------------------------------------------------------- /python-in-edu/resources/templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "resources/base.html" %} 2 | 3 | {% block content %} 4 | 5 | {% if validlink %} 6 | 7 |Please enter your new password twice so we can verify you typed it in correctly.
8 | 9 | 28 | 29 | {% else %} 30 | 31 |The password reset link was invalid, possibly because it has already been used. Please request a 32 | new password reset.
33 | 34 | {% endif %} 35 | 36 | {% endblock content %} 37 | -------------------------------------------------------------------------------- /python-in-edu/resources/templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "resources/base.html" %} 2 | 3 | {% block content %} 4 | 5 |Forgotten your password? Enter your email address below, and we’ll email instructions for setting 6 | a new one.
7 | 8 | 20 | 21 | 22 | {% endblock content %} 23 | 24 | -------------------------------------------------------------------------------- /python-in-edu/resources/templates/resources/add_resource.html: -------------------------------------------------------------------------------- 1 | {% extends "resources/base.html" %} 2 | 3 | {% block page_title %}Add Resource{% endblock page_title %} 4 | 5 | {% block content %} 6 | 7 |Country: {{ profile.country }}
{% endif %} 24 | {% if profile.roles %}Roles: {{ profile.roles }}
{% endif %} 25 | {% if profile.populations %}Populations worked with: {{ profile.populations }}
{% endif %} 26 | {% if profile.underrep %} 27 |This user works primarily with learners from a group underrepresented in the tech industry.
28 | {% endif %} 29 | {% if profile.psf_member %} 30 |This user is a member of the Python Software Foundation.
31 | {% endif %} 32 | 33 |This user hasn't submitted any resources yet.
48 | {% endfor %} 49 |30 |
31 | {% if object.url_description1 %}{{ object.url_description1 }}
{% endif %} 32 | {% if object.url2 %}33 | get the resource
{% endif %} 34 | {% if object.url_description2 %}{{ object.url_description2 }}
{% endif %} 35 | {% if object.url3 %}36 | get the resource
{% endif %} 37 | {% if object.url_description3 %}{{ object.url_description3 }}
{% endif %} 38 | 39 | 40 | {% if request.user == object.submitter %} 41 | 42 | 43 | {% endif %} 44 |Description: {{ object.description }}
52 |Created By: {{ object.attribution }}
53 | 54 |submitted by 56 | {{ object.submitter.username }}
57 | 58 |27 | | Resource Name | 28 |Submitter | 29 |Author | 30 |Description | 31 |Audience | 32 |Resource Type | 33 |Devices | 34 | 35 | 36 |Requires Signup | 37 |Language | 38 |Use Type | 39 |Requirements | 40 |License | 41 |Standards | 42 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
56 | | 57 | {{ resource.title }} 58 | | 59 |{{ resource.submitter }} | 60 |{{ resource.author_bio|truncatechars:10 }} | 61 |{{ resource.description|truncatechars:30 }} |
62 |
63 | {{ resource.get_audience_display }} | 64 |{{ resource.get_resource_type_display }} | 65 |{{ resource.get_devices_display }} | 66 | 67 | 68 |{{ resource.requires_signup}} | 69 |{{ resource.language }} | 70 |{{ resource.use_type }} | 71 |{{ resource.requirements }} | 72 |{{ resource.license }} | 73 |{{ resource.standards }} | 74 | 75 |
Check out our toolkits and guides. These resources are full of recommendations, tips, and curated resources put together by experienced Python educators.
2 | 3 | {% load static %} 4 |What Works in Teaching Python Toolkit
5 |Evidence-based teaching strategies
6 | 7 |Actions that support CS instruction at the primary and secondary levels
8 |Platforms for Teaching Python Guide
9 |A guide to selecting the right programming platform for your educational context
10 |Inclusive Teaching Practices Guide - Coming Soon
11 |A guide to planning with cultural knowledge, linguistic diversity, and learning differences in mind
12 | -------------------------------------------------------------------------------- /python-in-edu/resources/templates/text_includes/resource_search_text.html: -------------------------------------------------------------------------------- 1 | Search for education and training resources or contribute to the database. -------------------------------------------------------------------------------- /python-in-edu/resources/templates/text_includes/unpublished_resource_alert.html: -------------------------------------------------------------------------------- 1 | This resource has status: {{ status }}. Please note that no one else can view this resource besides you, the submitter. Please contact us if you have questions. -------------------------------------------------------------------------------- /python-in-edu/resources/templates/text_includes/welcome.html: -------------------------------------------------------------------------------- 1 | Welcome to the Python Software Foundation’s hub for all things education. 2 | Search a database of open education resources (OERs). 3 | Contribute to the OER database. 4 | Find tips and guides for teaching Python. 5 | Participate in an active community of Python educators and trainers from around the globe. -------------------------------------------------------------------------------- /python-in-edu/resources/tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.test import TestCase 4 | from django.contrib.auth.models import User 5 | from django.urls import reverse 6 | from django.core import mail 7 | 8 | from .models import Profile, Resource, resource_updated 9 | from . import choices 10 | 11 | 12 | class BaseTestCase(TestCase): 13 | 14 | def create_users(self): 15 | self.user = User.objects.create(username="A", email="fake@example.com") 16 | self.user2 = User.objects.create(username="B", email="fake2@example.com") 17 | 18 | def create_resources(self): 19 | self.basic_resource_data = { 20 | "title": "A Title", "url": "www.example.com", "submitter": self.user, 21 | "status": choices.ResourceStatusChoices.PROPOSED, 22 | "resource_type": choices.ResourceTypeChoices.PLATFORM_APP, 23 | "audience": choices.AudienceChoices.K_THROUGH_12, 24 | "language": "English", "requirements": "none", "license": "none" 25 | } 26 | self.resource_a = Resource.objects.create(**self.basic_resource_data) 27 | self.resource_b = Resource.objects.create(**self.basic_resource_data) 28 | 29 | def setUp(self): 30 | self.create_users() 31 | self.create_resources() 32 | 33 | 34 | class ResourceViewCase(BaseTestCase): 35 | 36 | def test_resource_list_contains_accepted_only(self): 37 | """The resource list contains only accepted resources.""" 38 | self.resource_a.status = choices.ResourceStatusChoices.ACCEPTED 39 | self.resource_a.save() 40 | response = self.client.get(reverse('resource_list')) 41 | self.assertEqual(list(response.context['resource_list']), [self.resource_a]) 42 | 43 | 44 | class EmailTest(BaseTestCase): 45 | 46 | def test_send_email(self): 47 | """Staff users but not other users are notified when a new resource is created.""" 48 | 49 | # Set user to staff 50 | self.user2.is_staff = True 51 | self.user2.save() 52 | 53 | # Create new resource 54 | resource_c = Resource.objects.create(**self.basic_resource_data) 55 | 56 | self.assertEqual(len(mail.outbox), 1) 57 | self.assertEqual(mail.outbox[0].subject, "A new resource has been proposed on Python In Education") 58 | -------------------------------------------------------------------------------- /python-in-edu/resources/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | 6 | urlpatterns = [ 7 | path('profile/