diff --git a/.gitignore b/.gitignore
index b6e4761..afd0044 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,6 @@ dmypy.json
# Pyre type checker
.pyre/
+
+# Pycharm stuff
+*.idea*
diff --git a/README.md b/README.md
index 315d3f7..7628309 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ All contributors must agree to abide by our [Code of Conduct](https://github.com
## Installation Guide
-In order to run this site locally, you'll want to clone this repository and install the requirements:
+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):
```
git clone https://github.com/psf/python-in-edu.git
@@ -18,7 +18,14 @@ source .venv/bin/activate
pip install -r requirements.txt
```
-You can then change directories into the python-in-edu folder and run the following command in the terminal:
+You can then change directories into the python-in-edu folder and build the database:
+
+```
+python manage.py migrate
+```
+
+
+To run the project locally, run the following command in the terminal:
```
python manage.py runserver
@@ -43,3 +50,28 @@ If you want to use or test email functionality locally, you'll need to [run a si
## Notes
We use the [Spirit project](https://spirit-project.com/) for our forums.
+
+---
+
+
Mac Troubleshooting
+
+### Postgres
+
+If you don't have an installation of Postgres on your system, you might run into the following error:
+
+```
+Error: pg_config executable not found.
+```
+
+[Install Postgres](https://postgresapp.com/) to resolve this issue.
+
+### Pillow
+
+If your Pillow installation fails during installing the requirements with the following message:
+
+```
+The headers or library files could not be found for jpeg,
+a required dependency when compiling Pillow from source.
+```
+
+You can resolve this by installing [jpeg](https://formulae.brew.sh/formula/jpeg) using [homebrew](https://brew.sh/).
\ No newline at end of file
diff --git a/python-in-edu/mysite/settings.py b/python-in-edu/mysite/settings.py
index bc7728f..076609c 100644
--- a/python-in-edu/mysite/settings.py
+++ b/python-in-edu/mysite/settings.py
@@ -171,6 +171,9 @@
}
}
+if 'FORCE_HTTPS' in os.environ: # running on heroku
+ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+ SECURE_SSL_REDIRECT = True
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
diff --git a/python-in-edu/resources/admin.py b/python-in-edu/resources/admin.py
index a31e416..2948c70 100644
--- a/python-in-edu/resources/admin.py
+++ b/python-in-edu/resources/admin.py
@@ -1,7 +1,175 @@
from django.contrib import admin
-from .models import Profile, Resource
+from .models import (
+ Profile,
+ Resource,
+ Author,
+ ProfilePopulation,
+ ProfileRole,
+ ResourceLanguage,
+ ResourceUseType,
+ ResourceAudience,
+ ResourceType,
+ SignupChoice,
+ ResourceStatus,
+ Device,
+ Organization
+)
-admin.site.register(Profile)
-admin.site.register(Resource)
\ No newline at end of file
+@admin.register(Profile)
+class ProfileAdmin(admin.ModelAdmin):
+ list_display = [
+ 'user',
+ 'organization',
+ 'country',
+ 'populations',
+ 'psf_member'
+ ]
+ list_filter = [
+ 'user',
+ 'organization',
+ 'country',
+ 'populations',
+ 'psf_member'
+ ]
+
+@admin.register(Author)
+class AuthorAdmin(admin.ModelAdmin):
+ list_display = [
+ 'name'
+ ]
+
+@admin.register(ProfilePopulation)
+class ProfilePopulationAdmin(admin.ModelAdmin):
+ list_display = [
+ 'name',
+ 'active',
+ 'underrepresented'
+ ]
+ list_filter = [
+ 'name',
+ 'active',
+ 'underrepresented'
+ ]
+
+@admin.register(ProfileRole)
+class ProfileRoleAdmin(admin.ModelAdmin):
+ list_display = [
+ 'name',
+ 'active'
+ ]
+ list_filter = [
+ 'name',
+ 'active'
+ ]
+
+
+@admin.register(Resource)
+class ResourceAdmin(admin.ModelAdmin):
+ list_display = [
+ 'title',
+ 'submitter',
+ 'status',
+ 'description'
+ ]
+ list_filter = [
+ 'submitter',
+ 'status'
+ ]
+
+@admin.register(ResourceLanguage)
+class ResourceLanguageAdmin(admin.ModelAdmin):
+ list_display = [
+ 'name',
+ 'description',
+ 'active'
+ ]
+ list_filter = [
+ 'name',
+ 'active'
+ ]
+
+
+@admin.register(ResourceUseType)
+class ResourceUseTypeAdmin(admin.ModelAdmin):
+ list_display = [
+ 'name',
+ 'description',
+ 'active'
+ ]
+ list_filter = [
+ 'active'
+ ]
+
+
+@admin.register(ResourceAudience)
+class ResourceAudienceAdmin(admin.ModelAdmin):
+ list_display = [
+ 'name',
+ 'description',
+ 'active'
+ ]
+ list_filter = [
+ 'active'
+ ]
+
+
+@admin.register(ResourceType)
+class ResourceTypeAdmin(admin.ModelAdmin):
+ list_display = [
+ 'name',
+ 'description',
+ 'active'
+ ]
+ list_filter = [
+ 'active'
+ ]
+
+
+@admin.register(SignupChoice)
+class SignupChoiceAdmin(admin.ModelAdmin):
+ list_display = [
+ 'name',
+ 'description',
+ 'active'
+ ]
+ list_filter = [
+ 'active'
+ ]
+
+
+@admin.register(ResourceStatus)
+class ResourceStatusAdmin(admin.ModelAdmin):
+ list_display = [
+ 'name',
+ 'description',
+ 'active',
+ 'sequence'
+ ]
+ list_filter = [
+ 'active'
+ ]
+
+
+@admin.register(Device)
+class DeviceAdmin(admin.ModelAdmin):
+ list_display = [
+ 'name',
+ 'active'
+ ]
+ list_filter = [
+ 'active'
+ ]
+
+
+@admin.register(Organization)
+class OrganizationAdmin(admin.ModelAdmin):
+ list_display = [
+ 'name',
+ 'description',
+ 'active'
+ ]
+ list_filter = [
+ 'active'
+ ]
diff --git a/python-in-edu/resources/choices.py b/python-in-edu/resources/choices.py
deleted file mode 100644
index c08439b..0000000
--- a/python-in-edu/resources/choices.py
+++ /dev/null
@@ -1,91 +0,0 @@
-from django.utils.translation import gettext_lazy as _
-from django.db import models
-
-
-class ResourceStatusChoices(models.TextChoices):
- PROPOSED = 'PR', _('Proposed')
- ACCEPTED = 'AC', _('Accepted')
- REJECTED = 'RJ', _('Rejected')
- WITHDRAWN = 'WD', _('Withdrawn')
-
-
-class ResourceTypeChoices(models.TextChoices):
- PLATFORM_APP = 'PA', _('Platform or App')
- CURRICULUM = 'CU', _('Curriculum')
- TUTORIAL_COURSE = 'TC', _('Tutorial or Course')
- BOOK = 'BK', _('Book')
- WORKED_EXAMPLE = 'WE', _('Worked Example')
- DOCUMENTATION = 'DC', _('Documentation')
- OTHER = 'OT', _('Other')
-
-
-class AudienceChoices(models.TextChoices):
- K_THROUGH_12 = 'K12', _('K-12')
- HIGHER_ED = 'HIE', _('Higher Education')
- PROFESSIONAL_TRAINING = 'PFT', _('Professional Training')
- NOT_SPECIFIC = 'NSP', _('Not Specific')
- OTHER = 'OTH', _('Other')
-
-
-class DeviceChoices(models.TextChoices):
- DESKTOP_OR = 'DOL', _('Desktop or Laptop Computer')
- CHROMEBOOK_OR = 'CON', _('Chromebook or Other Netbook')
- IPAD = 'IPD', _('iPad')
- ANDROID_TABLET = 'ATB', _('Android Tablet')
- IPHONE = 'IPH', _('iPhone')
- ANDROID_PHONE = 'APH', _('Android Phone')
- RASPBERRY_PI = 'RSP', _('Raspberry Pi')
- MICROCONTROLLERS = 'MCC', _('Microcontroller(s)')
- OTHER = 'OTH', _('Other')
-
-
-class UserRoleChoices(models.TextChoices):
- STUDENT = 'STD', _('Student')
- PS_EDUCATOR_SCHOOL = 'PES', _('Primary/Secondary Educator (school setting)')
- PS_EDUCATOR_OO_SCHOOL = 'PEO', _('Primary/Secondary Educator (out of school setting)')
- TEACHING_FACULTY = 'TF', _('Teaching Faculty (post-secondary)')
- ADULT_EDU_BOOTCAMP_ETC = 'AEB', _('Adult Educator or Trainer (bootcamp, industry)')
- ADULT_EDU_COACHING_ETC = 'AEC', _('Adult Educator or Trainer (teacher PD, coaching)')
- CURRICULUM_DEVELOPER = 'CUR', _('Curriculum or Product Developer')
- EDUCATION_VOLUNTEER = 'VOL', _('Education Volunteer')
- RESEARCHER = 'RES', _('Researcher')
- MENTOR = 'MNT', _('Mentor')
- INDUSTRY_PROF = 'INP', _('Industry Professional (Tech/Software/CS)')
- EDUCATION_DEVELOPER = 'EDV', _('Educational program developer')
- PARENT = 'PRT', _('Parent supporting education')
- OTHER = 'OTH', _('Other')
-
-
-class PopulationChoices(models.TextChoices):
- PRIMARY = 'PRI', _('Primary')
- SECONDAY = 'SEC', _('Secondary ')
- COLLEGE = 'COL', _('College/University')
- ADULT = 'ADU', _('Adult/Professional')
- OTHER = 'OTH', _('Other')
- NONE = 'NON', _('None')
-
-
-class UseTypeChoices(models.TextChoices):
- OPEN_SOURCE_PROJECT = 'OSP', _('Open Source Project - accepts contributions')
- OPEN_EDUCATION_RESOURCE = 'OER', _('Open Education Resource - ok to distribute and/or revise/remix')
- FREE_RESOURCE = 'FRE', _('Free Resource - free to use')
- FREEMIUM = 'IUM', _('Freemium - significant portion of resource free to use')
- PAID = 'PAI', _('Paid - costs money to access this resource')
- UNKOWN = 'UNK', _('Bleh')
-
-class PythonChoices(models.TextChoices):
- PYTHON_SPECIFIC = 'PS', _('Python Specific - part or all of resource is Python specific')
- LANGUAGE_AGNOSTIC = 'LA', _('Language Agnostic - can be used with any programming language')
- UNKNOWN = 'UN',_('Unkown')
-
-class SignUpChoices(models.TextChoices):
- CREATE_ACCOUNT = 'CA', _('Must create an account')
- PROVIDE_EMAIL = 'PE', _('Must provide email address')
- NO_REQUIREMENT = 'NR',_('No sign up requirement')
-
-
-
-
-
-
-
diff --git a/python-in-edu/resources/forms.py b/python-in-edu/resources/forms.py
new file mode 100644
index 0000000..72fff58
--- /dev/null
+++ b/python-in-edu/resources/forms.py
@@ -0,0 +1,41 @@
+from django import forms
+from .models import Resource, ResourceType, ResourceAudience, Device, ResourceUseType, ResourceLanguage
+
+
+class CreateResourceForm(forms.ModelForm):
+ class Meta:
+ model = Resource
+ # fields = '__all__'
+ fields = [
+ 'title',
+ 'description',
+ 'requires_signup',
+ 'license',
+ ]
+
+ description = forms.Textarea()
+
+ resource_types = forms.ModelMultipleChoiceField(
+ queryset=ResourceType.objects.all(),
+ widget=forms.CheckboxSelectMultiple
+ )
+
+ audience = forms.ModelMultipleChoiceField(
+ queryset=ResourceAudience.objects.all(),
+ widget=forms.CheckboxSelectMultiple
+ )
+
+ devices = forms.ModelMultipleChoiceField(
+ queryset=Device.objects.all(),
+ widget=forms.CheckboxSelectMultiple
+ )
+
+ use_type = forms.ModelMultipleChoiceField(
+ queryset=ResourceUseType.objects.all(),
+ widget=forms.CheckboxSelectMultiple
+ )
+
+ languages = forms.ModelMultipleChoiceField(
+ queryset=ResourceLanguage.objects.all(),
+ widget=forms.CheckboxSelectMultiple
+ )
diff --git a/python-in-edu/resources/migrations/0009_auto_20210516_1818.py b/python-in-edu/resources/migrations/0009_auto_20210516_1818.py
new file mode 100644
index 0000000..bc46036
--- /dev/null
+++ b/python-in-edu/resources/migrations/0009_auto_20210516_1818.py
@@ -0,0 +1,28 @@
+# Generated by Django 3.1.6 on 2021-05-16 18:18
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resources', '0008_auto_20210512_1226'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='resource',
+ name='attribution',
+ ),
+ migrations.CreateModel(
+ name='Author',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=100)),
+ ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
+ ('update_timestamp', models.DateTimeField(auto_now=True)),
+ ('resource', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='resources.resource')),
+ ],
+ ),
+ ]
diff --git a/python-in-edu/resources/migrations/0010_auto_20210516_1822.py b/python-in-edu/resources/migrations/0010_auto_20210516_1822.py
new file mode 100644
index 0000000..d97aea1
--- /dev/null
+++ b/python-in-edu/resources/migrations/0010_auto_20210516_1822.py
@@ -0,0 +1,22 @@
+# Generated by Django 3.1.6 on 2021-05-16 18:22
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resources', '0009_auto_20210516_1818'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='author',
+ name='resource',
+ ),
+ migrations.AddField(
+ model_name='resource',
+ name='author',
+ field=models.ManyToManyField(to='resources.Author'),
+ ),
+ ]
diff --git a/python-in-edu/resources/migrations/0011_auto_20210520_2114.py b/python-in-edu/resources/migrations/0011_auto_20210520_2114.py
new file mode 100644
index 0000000..f5635ef
--- /dev/null
+++ b/python-in-edu/resources/migrations/0011_auto_20210520_2114.py
@@ -0,0 +1,242 @@
+# Generated by Django 3.1.6 on 2021-05-20 21:14
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('resources', '0010_auto_20210516_1822'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Device',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=100)),
+ ('active', models.BooleanField(default=True)),
+ ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
+ ('update_timestamp', models.DateTimeField(auto_now=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Organization',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=50)),
+ ('description', models.CharField(max_length=200)),
+ ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
+ ('update_timestamp', models.DateTimeField(auto_now=True)),
+ ('active', models.BooleanField(default=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ProfilePopulation',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=100)),
+ ('active', models.BooleanField(default=True)),
+ ('underrepresented', models.BooleanField(default=False)),
+ ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
+ ('update_timestamp', models.DateTimeField(auto_now=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ProfileRole',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=100)),
+ ('active', models.BooleanField(default=True)),
+ ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
+ ('update_timestamp', models.DateTimeField(auto_now=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ResourceAudience',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=50)),
+ ('description', models.CharField(max_length=200)),
+ ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
+ ('update_timestamp', models.DateTimeField(auto_now=True)),
+ ('active', models.BooleanField(default=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ResourceLanguages',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=50)),
+ ('description', models.CharField(max_length=200)),
+ ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
+ ('update_timestamp', models.DateTimeField(auto_now=True)),
+ ('active', models.BooleanField(default=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ResourceStatus',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=50)),
+ ('description', models.CharField(max_length=200)),
+ ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
+ ('update_timestamp', models.DateTimeField(auto_now=True)),
+ ('active', models.BooleanField(default=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ResourceType',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=50)),
+ ('description', models.CharField(max_length=200)),
+ ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
+ ('update_timestamp', models.DateTimeField(auto_now=True)),
+ ('active', models.BooleanField(default=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ResourceUseType',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=50)),
+ ('description', models.CharField(max_length=200)),
+ ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
+ ('update_timestamp', models.DateTimeField(auto_now=True)),
+ ('active', models.BooleanField(default=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='SignupChoice',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=50)),
+ ('description', models.CharField(max_length=200)),
+ ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
+ ('update_timestamp', models.DateTimeField(auto_now=True)),
+ ('active', models.BooleanField(default=True)),
+ ],
+ ),
+ migrations.RemoveField(
+ model_name='profile',
+ name='underrep',
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='contact',
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='python_related',
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='resource_type',
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='url1',
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='url2',
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='url3',
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='url_description1',
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='url_description2',
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='url_description3',
+ ),
+ migrations.AddField(
+ model_name='author',
+ name='biography',
+ field=models.TextField(blank=True, null=True),
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='audience',
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='devices',
+ ),
+ migrations.AlterField(
+ model_name='resource',
+ name='submitter',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.RemoveField(
+ model_name='resource',
+ name='use_type',
+ ),
+ migrations.CreateModel(
+ name='ResourceURL',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('url', models.URLField()),
+ ('description', models.TextField()),
+ ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
+ ('update_timestamp', models.DateTimeField(auto_now=True)),
+ ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='url_resource', to='resources.resource')),
+ ],
+ ),
+ migrations.AddField(
+ model_name='resource',
+ name='languages',
+ field=models.ManyToManyField(help_text='Choose the languages that your resource focuses on.', limit_choices_to={'active': True}, to='resources.ResourceLanguages'),
+ ),
+ migrations.AddField(
+ model_name='resource',
+ name='resource_types',
+ field=models.ManyToManyField(help_text='Select all that apply.', limit_choices_to={'active': True}, to='resources.ResourceType'),
+ ),
+ migrations.AlterField(
+ model_name='profile',
+ name='populations',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='resources.profilepopulation'),
+ ),
+ migrations.AlterField(
+ model_name='profile',
+ name='roles',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='resources.profilerole'),
+ ),
+ migrations.AddField(
+ model_name='resource',
+ name='audience',
+ field=models.ManyToManyField(help_text="Select 'not specific' for resources for any or all audiences.", limit_choices_to={'active': True}, to='resources.ResourceAudience'),
+ ),
+ migrations.AddField(
+ model_name='resource',
+ name='devices',
+ field=models.ManyToManyField(help_text='Which devices are compatible with this resource', limit_choices_to={'active': True}, to='resources.Device'),
+ ),
+ migrations.AlterField(
+ model_name='resource',
+ name='requires_signup',
+ field=models.ForeignKey(help_text='Are users required to create an account or provide their email address to access this resource?', on_delete=django.db.models.deletion.PROTECT, to='resources.signupchoice'),
+ ),
+ migrations.AlterField(
+ model_name='resource',
+ name='status',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='resources.resourcestatus'),
+ ),
+ migrations.AddField(
+ model_name='resource',
+ name='use_type',
+ field=models.ManyToManyField(help_text='Select the use type that best describes this resource.', limit_choices_to={'active': True}, to='resources.ResourceUseType'),
+ ),
+ ]
diff --git a/python-in-edu/resources/migrations/0012_auto_20210520_2251.py b/python-in-edu/resources/migrations/0012_auto_20210520_2251.py
new file mode 100644
index 0000000..410d52d
--- /dev/null
+++ b/python-in-edu/resources/migrations/0012_auto_20210520_2251.py
@@ -0,0 +1,22 @@
+# Generated by Django 3.1.6 on 2021-05-20 22:51
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resources', '0011_auto_20210520_2114'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='resource',
+ name='language',
+ ),
+ migrations.AddField(
+ model_name='resourcestatus',
+ name='sequence',
+ field=models.IntegerField(default=1),
+ ),
+ ]
diff --git a/python-in-edu/resources/migrations/0013_auto_20210521_0415.py b/python-in-edu/resources/migrations/0013_auto_20210521_0415.py
new file mode 100644
index 0000000..80f63e9
--- /dev/null
+++ b/python-in-edu/resources/migrations/0013_auto_20210521_0415.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.6 on 2021-05-21 04:15
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resources', '0012_auto_20210520_2251'),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name='ResourceLanguages',
+ new_name='ResourceLanguage',
+ ),
+ migrations.AlterField(
+ model_name='resource',
+ name='status',
+ field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to='resources.resourcestatus'),
+ ),
+ ]
diff --git a/python-in-edu/resources/migrations/0014_auto_20210607_1959.py b/python-in-edu/resources/migrations/0014_auto_20210607_1959.py
new file mode 100644
index 0000000..483b581
--- /dev/null
+++ b/python-in-edu/resources/migrations/0014_auto_20210607_1959.py
@@ -0,0 +1,125 @@
+# Generated by Django 3.1.6 on 2021-06-07 19:59
+
+from django.db import migrations
+
+def load_organizations(apps, schema_editor):
+ Organization = apps.get_model('resources', 'Organization')
+ pass
+
+def load_profile_roles(apps, schema_editor):
+ ProfileRole = apps.get_model('resources', 'ProfileRole')
+
+ ProfileRole.objects.get_or_create(pk=1, name='Student', active=True)
+ ProfileRole.objects.get_or_create(pk=2, name='Educator', active=True)
+ pass
+
+def load_profile_populations(apps, schema_editor):
+ ProfilePopulation = apps.get_model('resources', 'ProfilePopulation')
+
+ ProfilePopulation.objects.get_or_create(pk=1, name='Primary', active=True, underrepresented=False)
+ ProfilePopulation.objects.get_or_create(pk=2, name='Secondary', active=True, underrepresented=False)
+ ProfilePopulation.objects.get_or_create(pk=3, name='College', active=True, underrepresented=False)
+ ProfilePopulation.objects.get_or_create(pk=4, name='Adult', active=True, underrepresented=False)
+ ProfilePopulation.objects.get_or_create(pk=5, name='Other', active=True, underrepresented=False)
+ ProfilePopulation.objects.get_or_create(pk=6, name='None', active=True, underrepresented=False)
+
+def load_devices(apps, schema_editor):
+ Device = apps.get_model('resources', 'Device')
+
+ Device.objects.get_or_create(pk=1, name='IPhone', active=True)
+ Device.objects.get_or_create(pk=2, name='Desktop/Laptop', active=True)
+ Device.objects.get_or_create(pk=3, name='Chromebook', active=True)
+ Device.objects.get_or_create(pk=4, name='IPad', active=True)
+ Device.objects.get_or_create(pk=5, name='Android Tablet', active=True)
+ Device.objects.get_or_create(pk=6, name='Android Phone', active=True)
+ Device.objects.get_or_create(pk=7, name='Raspberry Pi', active=True)
+ Device.objects.get_or_create(pk=8, name='Microcontrollers', active=True)
+
+def load_resource_statuses(apps, schema_editor):
+ ResourceStatus = apps.get_model('resources', 'ResourceStatus')
+
+ ResourceStatus.objects.get_or_create(pk=1, name='Proposed',
+ description='Resource has been proposed and is in review.', active=True,
+ sequence=1)
+ ResourceStatus.objects.get_or_create(pk=2, name='Accepted',
+ description='Resource has been reviewed and accepted.', active=True,
+ sequence=2)
+ ResourceStatus.objects.get_or_create(pk=3, name='Rejected',
+ description='Resource has been reviewed and rejected.', active=True,
+ sequence=3)
+ ResourceStatus.objects.get_or_create(pk=4, name='Withdrawn',
+ description='Resource has been withdrawn from review.', active=True,
+ sequence=4)
+
+
+def load_signup_choices(apps, schema_editor):
+ SignupChoice = apps.get_model('resources', 'SignupChoice')
+
+ SignupChoice.objects.get_or_create(pk=1, name='Create Account', description='Must create an account', active=True)
+ SignupChoice.objects.get_or_create(pk=2, name='Provide Email', description='Must provide email address', active=True)
+ SignupChoice.objects.get_or_create(pk=3, name='No Requirement', description='No sign up requirement', active=True)
+
+def load_resource_types(apps, schema_editor):
+ ResourceType = apps.get_model('resources', 'ResourceType')
+
+ ResourceType.objects.get_or_create(pk=1, name='Platform or App',
+ description='Resource is specific to a platform or app', active=True)
+ ResourceType.objects.get_or_create(pk=2, name='Curriculum',
+ description='Resource is intended to be part of a curriculum.', active=True)
+ ResourceType.objects.get_or_create(pk=3, name='Tutorial',
+ description='Resource is part of a tutorial course.', active=True)
+ ResourceType.objects.get_or_create(pk=4, name='Book',
+ description='Resource is included as part of a book.', active=True)
+ ResourceType.objects.get_or_create(pk=5, name='Worked Example',
+ description='Resource is part of a worked example.', active=True)
+ ResourceType.objects.get_or_create(pk=6, name='Documentation',
+ description='Resource is part of documentation.', active=True)
+
+
+def load_resource_audiences(apps, schema_editor):
+ ResourceAudience = apps.get_model('resources', 'ResourceAudience')
+
+ ResourceAudience.objects.get_or_create(pk=1, name='K-12', description='K-12', active=True)
+ ResourceAudience.objects.get_or_create(pk=2, name='Higher Education', description='College/Secondary School', active=True)
+ ResourceAudience.objects.get_or_create(pk=3, name='Professional Training', description='Meant for professionals', active=True)
+ ResourceAudience.objects.get_or_create(pk=4, name='General', description='General audience', active=True)
+
+def load_resource_use_types(apps, schema_editor):
+ ResourceUseType = apps.get_model('resources', 'ResourceUseType')
+
+ ResourceUseType.objects.get_or_create(pk=1, name='Open Source Project',
+ description='Open Source Project - accepts contributions', active=True)
+ ResourceUseType.objects.get_or_create(pk=2, name='Open Education Resource',
+ description='Open Education Resource - ok to distribute and/or revise/remix.', active=True)
+ ResourceUseType.objects.get_or_create(pk=3, name='Free Resource',
+ description='Free Resource - Free to use', active=True)
+ ResourceUseType.objects.get_or_create(pk=4, name='Freemium',
+ description='Freemium - significant portion of resource free to use', active=True)
+ ResourceUseType.objects.get_or_create(pk=5, name='Paid',
+ description='Paid - costs money to access this resource', active=True)
+
+
+def load_resource_languages(apps, schema_editor):
+ ResourceLanguage = apps.get_model('resources', 'ResourceLanguage')
+
+ ResourceLanguage.objects.get_or_create(pk=1, name='Python', description='Best language ever!', active=True)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resources', '0013_auto_20210521_0415'),
+ ]
+
+ operations = [
+ migrations.RunPython(load_organizations),
+ migrations.RunPython(load_profile_roles),
+ migrations.RunPython(load_profile_populations),
+ migrations.RunPython(load_devices),
+ migrations.RunPython(load_resource_statuses),
+ migrations.RunPython(load_signup_choices),
+ migrations.RunPython(load_resource_types),
+ migrations.RunPython(load_resource_audiences),
+ migrations.RunPython(load_resource_use_types),
+ migrations.RunPython(load_resource_languages)
+ ]
diff --git a/python-in-edu/resources/migrations/0015_auto_20210608_2119.py b/python-in-edu/resources/migrations/0015_auto_20210608_2119.py
new file mode 100644
index 0000000..7afdc40
--- /dev/null
+++ b/python-in-edu/resources/migrations/0015_auto_20210608_2119.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.1.6 on 2021-06-08 21:19
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resources', '0014_auto_20210607_1959'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='resource',
+ name='status',
+ field=models.ForeignKey(blank=True, default=1, null=True, on_delete=django.db.models.deletion.PROTECT, to='resources.resourcestatus'),
+ ),
+ ]
diff --git a/python-in-edu/resources/models.py b/python-in-edu/resources/models.py
index c08eff0..29fa2ce 100644
--- a/python-in-edu/resources/models.py
+++ b/python-in-edu/resources/models.py
@@ -4,24 +4,56 @@
from django.core.mail import send_mail
from django.urls import reverse
-from multiselectfield import MultiSelectField
-
from mysite.settings import DEFAULT_FROM_EMAIL
-from . import choices
+def get_initial_status():
+ try:
+ initial_status = ResourceStatus.objects.get(sequence=1).id
+ except ResourceStatus.DoesNotExist:
+ initial_status = None
+ return initial_status
+
# Profile Models
+class Organization(models.Model):
+ name = models.CharField(max_length=50)
+ description = models.CharField(max_length=200)
+ creation_timestamp = models.DateTimeField(auto_now_add=True)
+ update_timestamp = models.DateTimeField(auto_now=True)
+ active = models.BooleanField(default=True)
+
+ def __str__(self):
+ return f'{self.name}-{self.description}'
-class Profile(models.Model):
+class ProfileRole(models.Model):
+ name = models.CharField(max_length=100)
+ active = models.BooleanField(default=True)
+ creation_timestamp = models.DateTimeField(auto_now_add=True)
+ update_timestamp = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ return f'{self.name}'
+
+
+class ProfilePopulation(models.Model):
+ name = models.CharField(max_length=100)
+ active = models.BooleanField(default=True)
+ underrepresented = models.BooleanField(default=False)
+ creation_timestamp = models.DateTimeField(auto_now_add=True)
+ update_timestamp = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ return f'{self.name}'
+
+class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
organization = models.CharField(max_length=100, blank=True, null=True)
country = models.CharField(max_length=100, blank=True, null=True)
- roles = MultiSelectField(choices=choices.UserRoleChoices.choices)
- populations = MultiSelectField(choices=choices.PopulationChoices.choices)
- underrep = models.BooleanField(default=False)
- psf_member = models.BooleanField(default=False)
+ roles = models.ForeignKey(ProfileRole, on_delete=models.PROTECT)
+ populations = models.ForeignKey(ProfilePopulation, on_delete=models.PROTECT)
+ psf_member = models.BooleanField(default=False) #TODO couldn't this be automatically defined with data from PSF?
def __str__(self):
return f"{self.user.username}"
@@ -36,49 +68,126 @@ def create_user_profile(sender, instance, created, **kwargs):
# Resource Models
-#class Link(models.Model):
+class Author(models.Model):
+ name = models.CharField(max_length=100)
+ biography = models.TextField(blank=True, null=True)
+ creation_timestamp = models.DateTimeField(auto_now_add=True)
+ update_timestamp = models.DateTimeField(auto_now=True)
+ def __str__(self):
+ return self.name
-class Resource(models.Model):
- #Required and optional fields
- url1 = models.CharField(max_length=200, help_text="You must link at least one resource.")
- url_description1 = models.CharField(max_length=50, blank=True, null=True, help_text="Use this field, if you are including multiple urls")
- # resource = models.ForeignKey('Resource', on_delete=models.CASCADE, related_name='links')
- url2 = models.CharField(max_length=200, blank=True, null=True, help_text="Optional additional url related to the same resource")
- url_description2 = models.CharField(max_length=50, blank=True, null=True, help_text="Use this field, if you are including multiple urls")
- # resource = models.ForeignKey('Resource', on_delete=models.CASCADE, related_name='links')
- url3 = models.CharField(max_length=200, blank=True, null=True, help_text="Optional additional url related to the same resource")
- url_description3 = models.CharField(max_length=50, blank=True, null=True, help_text="Use this field, if you are including multiple urls")
- # resource = models.ForeignKey('Resource', on_delete=models.CASCADE, related_name='links')
+class Device(models.Model):
+ name = models.CharField(max_length=100)
+ active = models.BooleanField(default=True)
+ creation_timestamp = models.DateTimeField(auto_now_add=True)
+ update_timestamp = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ return f'{self.name}'
+
+
+class ResourceStatus(models.Model):
+ name = models.CharField(max_length=50)
+ description = models.CharField(max_length=200)
+ creation_timestamp = models.DateTimeField(auto_now_add=True)
+ update_timestamp = models.DateTimeField(auto_now=True)
+ active = models.BooleanField(default=True)
+ sequence = models.IntegerField(default=1)
+
+ def __str__(self):
+ return f'{self.name}'
+
+
+class SignupChoice(models.Model):
+ name = models.CharField(max_length=50)
+ description = models.CharField(max_length=200)
+ creation_timestamp = models.DateTimeField(auto_now_add=True)
+ update_timestamp = models.DateTimeField(auto_now=True)
+ active = models.BooleanField(default=True)
+
+ def __str__(self):
+ return f'{self.name}'
+
+
+class ResourceType(models.Model):
+ name = models.CharField(max_length=50)
+ description = models.CharField(max_length=200)
+ creation_timestamp = models.DateTimeField(auto_now_add=True)
+ update_timestamp = models.DateTimeField(auto_now=True)
+ active = models.BooleanField(default=True)
+
+ def __str__(self):
+ return f'{self.name}'
+
+
+class ResourceAudience(models.Model):
+ name = models.CharField(max_length=50)
+ description = models.CharField(max_length=200)
+ creation_timestamp = models.DateTimeField(auto_now_add=True)
+ update_timestamp = models.DateTimeField(auto_now=True)
+ active = models.BooleanField(default=True)
+
+ def __str__(self):
+ return f'{self.name}'
+
+
+class ResourceUseType(models.Model):
+ name = models.CharField(max_length=50)
+ description = models.CharField(max_length=200)
+ creation_timestamp = models.DateTimeField(auto_now_add=True)
+ update_timestamp = models.DateTimeField(auto_now=True)
+ active = models.BooleanField(default=True)
+
+ def __str__(self):
+ return f'{self.name}'
+
+
+class ResourceLanguage(models.Model):
+ name = models.CharField(max_length=50)
+ description = models.CharField(max_length=200)
+ creation_timestamp = models.DateTimeField(auto_now_add=True)
+ update_timestamp = models.DateTimeField(auto_now=True)
+ active = models.BooleanField(default=True)
+
+ def __str__(self):
+ return f'{self.name}'
+
+class Resource(models.Model):
# core fields
title = models.CharField(max_length=200, help_text="What is the name of the resource")
- submitter = models.ForeignKey(User, on_delete=models.CASCADE) # FIXME: probably want to orphan rather than delete
- status = models.CharField(max_length=3, choices=choices.ResourceStatusChoices.choices, default=choices.ResourceStatusChoices.PROPOSED)
+ submitter = models.ForeignKey(User, on_delete=models.DO_NOTHING)
+ status = models.ForeignKey(ResourceStatus, on_delete=models.PROTECT, default=get_initial_status(), blank=True, null=True)
# required fields
- 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?")
- resource_type = MultiSelectField(max_length=30,choices=choices.ResourceTypeChoices.choices, help_text="Select all that apply.")
- audience = MultiSelectField(max_length=30,choices=choices.AudienceChoices.choices, help_text="Select 'not specific' for resources for any or all audiences.")
- devices = MultiSelectField(max_length=30,choices=choices.DeviceChoices.choices, help_text="Which devices are compatible with this resource")
+ requires_signup = models.ForeignKey(SignupChoice, on_delete=models.PROTECT, help_text="Are users required to create an account or provide their email address to access this resource?")
+ resource_types = models.ManyToManyField(ResourceType, help_text="Select all that apply.", limit_choices_to={'active': True})
+ audience = models.ManyToManyField(ResourceAudience, help_text="Select 'not specific' for resources for any or all audiences.", limit_choices_to={'active': True})
+ devices = models.ManyToManyField(Device, help_text="Which devices are compatible with this resource", limit_choices_to={'active': True})
description = models.CharField(max_length=500, help_text="Add a description of this resource. (max 500 characters)")
- attribution = models.CharField(max_length=250, help_text="What person or organization created this resource?")
- 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)
- 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)
+ author = models.ManyToManyField(Author)
+ use_type = models.ManyToManyField(ResourceUseType, help_text="Select the use type that best describes this resource.", limit_choices_to={'active': True})
# optional fields
-
- #author_bio = models.CharField(max_length=250, blank=True, null=True)
- #organization = models.CharField(max_length=250, blank=True, null=True)
- 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?")
- #standards = models.CharField(max_length=250, blank=True, null=True)
- language = models.CharField(max_length=50, blank=True, null=True, help_text="What language/s are the written materials available in?")
- #requirements = models.CharField(max_length=200, blank=True, null=True)
+ languages = models.ManyToManyField(ResourceLanguage, help_text="Choose the languages that your resource focuses on.", limit_choices_to={'active': True})
+
+ # TODO replace the contact field with contact information against the Profile model
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.")
def __str__(self):
- return f"{self.title} (submitted by {self.submitter}) - {self.get_status_display()}"
+ return f"{self.title} (submitted by {self.submitter}) - {self.status.name}"
+
+
+class ResourceURL(models.Model):
+ resource = models.ForeignKey(Resource, on_delete=models.CASCADE, related_name='url_resource')
+ url = models.URLField(max_length=200)
+ description = models.TextField()
+ creation_timestamp = models.DateTimeField(auto_now_add=True)
+ update_timestamp = models.DateTimeField(auto_now=True)
+
+
def resource_updated(sender, instance, created, **kwargs):
diff --git a/python-in-edu/resources/templates/authors/author_create.html b/python-in-edu/resources/templates/authors/author_create.html
new file mode 100644
index 0000000..b575198
--- /dev/null
+++ b/python-in-edu/resources/templates/authors/author_create.html
@@ -0,0 +1,11 @@
+{% extends "resources/base.html" %}
+
+{% block page_title %}Add Author{% endblock page_title %}
+
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/python-in-edu/resources/templates/authors/author_list.html b/python-in-edu/resources/templates/authors/author_list.html
new file mode 100644
index 0000000..bc1d5e6
--- /dev/null
+++ b/python-in-edu/resources/templates/authors/author_list.html
@@ -0,0 +1,151 @@
+{% extends "resources/base.html" %}
+
+{% block page_title %}Resources{% endblock page_title %}
+
+{% block full_width_content %}
+
+