diff --git a/competencies/__init__.py b/competencies/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/competencies/admin.py b/competencies/admin.py
new file mode 100644
index 00000000..c32ae04b
--- /dev/null
+++ b/competencies/admin.py
@@ -0,0 +1,10 @@
+from django.contrib import admin
+
+from competencies.models import (
+ Competency, CompetencyDifficulty,
+ CompetencyAssessment, CompetencyAssessmentRating)
+
+admin.site.register(CompetencyDifficulty)
+admin.site.register(Competency)
+admin.site.register(CompetencyAssessment)
+admin.site.register(CompetencyAssessmentRating)
diff --git a/competencies/apps.py b/competencies/apps.py
new file mode 100644
index 00000000..29035a59
--- /dev/null
+++ b/competencies/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class CompetenciesConfig(AppConfig):
+ name = 'competencies'
diff --git a/competencies/fixtures/competencies.json b/competencies/fixtures/competencies.json
new file mode 100644
index 00000000..900586c1
--- /dev/null
+++ b/competencies/fixtures/competencies.json
@@ -0,0 +1,221 @@
+[
+ {
+ "model": "competencies.competencydifficulty",
+ "pk": 1,
+ "fields": {
+ "created": "2022-08-05T11:37:54.601Z",
+ "updated": "2022-08-05T11:37:54.601Z",
+ "created_by": 1,
+ "display_name": "Easy",
+ "numeric_difficulty": 1.0
+ }
+ },
+ {
+ "model": "competencies.competencydifficulty",
+ "pk": 2,
+ "fields": {
+ "created": "2022-08-05T11:40:28.237Z",
+ "updated": "2022-08-05T11:40:28.237Z",
+ "created_by": 1,
+ "display_name": "Intermediate",
+ "numeric_difficulty": 2.0
+ }
+ },
+ {
+ "model": "competencies.competencydifficulty",
+ "pk": 3,
+ "fields": {
+ "created": "2022-08-05T11:43:34.855Z",
+ "updated": "2022-08-05T11:43:34.855Z",
+ "created_by": 1,
+ "display_name": "Hard",
+ "numeric_difficulty": 4.0
+ }
+ },
+ {
+ "model": "competencies.competencydifficulty",
+ "pk": 4,
+ "fields": {
+ "created": "2022-08-05T13:57:47.318Z",
+ "updated": "2022-08-05T13:57:47.318Z",
+ "created_by": 1,
+ "display_name": "Very Hard",
+ "numeric_difficulty": 5.0
+ }
+ },
+ {
+ "model": "competencies.competencydifficulty",
+ "pk": 5,
+ "fields": {
+ "created": "2022-08-05T13:59:26.450Z",
+ "updated": "2022-08-05T13:59:26.451Z",
+ "created_by": 1,
+ "display_name": "Technical",
+ "numeric_difficulty": 1.2
+ }
+ },
+ {
+ "model": "competencies.competencydifficulty",
+ "pk": 6,
+ "fields": {
+ "created": "2022-08-05T13:59:45.617Z",
+ "updated": "2022-08-05T13:59:45.617Z",
+ "created_by": 1,
+ "display_name": "Normal",
+ "numeric_difficulty": 2.5
+ }
+ },
+ {
+ "model": "competencies.competencydifficulty",
+ "pk": 7,
+ "fields": {
+ "created": "2022-08-05T14:02:05.685Z",
+ "updated": "2022-08-05T14:02:05.685Z",
+ "created_by": 1,
+ "display_name": "Noob",
+ "numeric_difficulty": 0.5
+ }
+ },
+ {
+ "model": "competencies.competencydifficulty",
+ "pk": 8,
+ "fields": {
+ "created": "2022-08-05T14:02:32.024Z",
+ "updated": "2022-08-05T14:02:32.024Z",
+ "created_by": 1,
+ "display_name": "Noobs",
+ "numeric_difficulty": 0.6
+ }
+ },
+ {
+ "model": "competencies.competencydifficulty",
+ "pk": 9,
+ "fields": {
+ "created": "2022-08-05T14:02:56.550Z",
+ "updated": "2022-08-05T14:02:56.550Z",
+ "created_by": 1,
+ "display_name": "Nooobs!",
+ "numeric_difficulty": 0.7
+ }
+ },
+ {
+ "model": "competencies.competency",
+ "pk": 1,
+ "fields": {
+ "created": "2022-08-05T14:17:38.220Z",
+ "updated": "2022-08-05T14:17:38.220Z",
+ "created_by": 1,
+ "display_name": "HTML",
+ "perceived_difficulty": 1,
+ "is_visible": true
+ }
+ },
+ {
+ "model": "competencies.competency",
+ "pk": 2,
+ "fields": {
+ "created": "2022-08-05T14:23:41.673Z",
+ "updated": "2022-08-05T14:56:13.462Z",
+ "created_by": 1,
+ "display_name": "CSS",
+ "perceived_difficulty": 1,
+ "is_visible": true
+ }
+ },
+ {
+ "model": "competencies.competency",
+ "pk": 3,
+ "fields": {
+ "created": "2022-08-05T14:23:49.767Z",
+ "updated": "2022-08-05T14:23:49.767Z",
+ "created_by": 1,
+ "display_name": "JavaScript",
+ "perceived_difficulty": 2,
+ "is_visible": true
+ }
+ },
+ {
+ "model": "competencies.competency",
+ "pk": 4,
+ "fields": {
+ "created": "2022-08-05T14:24:11.521Z",
+ "updated": "2022-08-05T14:24:11.521Z",
+ "created_by": 1,
+ "display_name": "Python",
+ "perceived_difficulty": 3,
+ "is_visible": true
+ }
+ },
+ {
+ "model": "competencies.competency",
+ "pk": 5,
+ "fields": {
+ "created": "2022-08-05T14:24:17.102Z",
+ "updated": "2022-08-05T14:24:17.102Z",
+ "created_by": 1,
+ "display_name": "Django",
+ "perceived_difficulty": 4,
+ "is_visible": true
+ }
+ },
+ {
+ "model": "competencies.competency",
+ "pk": 6,
+ "fields": {
+ "created": "2022-08-05T14:24:23.195Z",
+ "updated": "2022-08-05T14:24:23.195Z",
+ "created_by": 1,
+ "display_name": "Kanban",
+ "perceived_difficulty": 1,
+ "is_visible": true
+ }
+ },
+ {
+ "model": "competencies.competency",
+ "pk": 7,
+ "fields": {
+ "created": "2022-08-05T14:24:29.380Z",
+ "updated": "2022-08-05T14:24:29.380Z",
+ "created_by": 1,
+ "display_name": "Readme",
+ "perceived_difficulty": 1,
+ "is_visible": true
+ }
+ },
+ {
+ "model": "competencies.competency",
+ "pk": 8,
+ "fields": {
+ "created": "2022-08-05T14:24:35.502Z",
+ "updated": "2022-08-05T14:24:35.502Z",
+ "created_by": 1,
+ "display_name": "Wireframes",
+ "perceived_difficulty": 1,
+ "is_visible": true
+ }
+ },
+ {
+ "model": "competencies.competency",
+ "pk": 9,
+ "fields": {
+ "created": "2022-08-05T14:24:42.333Z",
+ "updated": "2022-08-05T14:24:42.333Z",
+ "created_by": 1,
+ "display_name": "SCRUM",
+ "perceived_difficulty": 2,
+ "is_visible": true
+ }
+ },
+ {
+ "model": "competencies.competency",
+ "pk": 10,
+ "fields": {
+ "created": "2022-08-05T14:24:48.822Z",
+ "updated": "2022-08-05T14:24:48.822Z",
+ "created_by": 1,
+ "display_name": "Presenting",
+ "perceived_difficulty": 1,
+ "is_visible": true
+ }
+ }
+]
diff --git a/competencies/forms.py b/competencies/forms.py
new file mode 100644
index 00000000..8e47f794
--- /dev/null
+++ b/competencies/forms.py
@@ -0,0 +1,54 @@
+from django import forms
+from django.forms import BaseModelFormSet
+
+from competencies.models import (
+ Competency, CompetencyDifficulty,
+ CompetencyAssessment, CompetencyAssessmentRating)
+
+
+class RequiredModelFormSet(BaseModelFormSet):
+ def __init__(self, *args, **kwargs):
+ super(RequiredModelFormSet, self).__init__(*args, **kwargs)
+ for form in self.forms:
+ form.empty_permitted = False
+ form.use_required_attribute = True
+
+
+class CompetencyForm(forms.ModelForm):
+ perceived_difficulty = forms.ModelChoiceField(
+ queryset=CompetencyDifficulty.objects.all(),
+ widget=forms.Select(attrs={
+ 'class': 'form-control'
+ }))
+
+ class Meta:
+ model = Competency
+ fields = ['display_name', 'perceived_difficulty']
+
+
+class CompetencyDifficultyForm(forms.ModelForm):
+ class Meta:
+ model = CompetencyDifficulty
+ fields = ['display_name', 'numeric_difficulty']
+
+
+class CompetencyAssessmentForm(forms.ModelForm):
+ is_visible = forms.BooleanField(
+ required=False,
+ label="Make my assessment visible to my teams and facilitators")
+
+ class Meta:
+ model = CompetencyAssessment
+ fields = ['user', 'is_visible']
+
+
+class CompetencyAssessmentRatingForm(forms.ModelForm):
+ competency = forms.ModelChoiceField(
+ queryset=Competency.objects.all(),
+ widget=forms.Select(attrs={
+ 'class': 'form-control disabled-select',
+ }))
+
+ class Meta:
+ model = CompetencyAssessmentRating
+ fields = ['id', 'user_assessment', 'competency', 'rating']
diff --git a/competencies/helpers.py b/competencies/helpers.py
new file mode 100644
index 00000000..03a9e938
--- /dev/null
+++ b/competencies/helpers.py
@@ -0,0 +1,30 @@
+from django.core.exceptions import ObjectDoesNotExist
+
+from competencies.forms import CompetencyAssessmentForm
+from competencies.models import CompetencyAssessment
+
+
+def get_or_create_competency_assessment(data):
+ try:
+ existing_assessment = CompetencyAssessment.objects.get(
+ user=data.get('user'))
+ existing_assessment.is_visible = data.get('is_visible') == 'on'
+ existing_assessment.save()
+ return existing_assessment
+ except ObjectDoesNotExist:
+ # If no assessment exists, just continue
+ pass
+
+ form = CompetencyAssessmentForm(data)
+ if form.is_valid():
+ return form.save()
+ return
+
+
+def populate_competency_assessment_for_formset(competency_assessment, data):
+ keys = [key for key in data.keys()
+ if key.endswith('-competency')]
+
+ for key in keys:
+ assessment_key = key.replace('-competency', '-user_assessment')
+ data[assessment_key] = competency_assessment.id
diff --git a/competencies/migrations/0001_initial.py b/competencies/migrations/0001_initial.py
new file mode 100644
index 00000000..44feeef7
--- /dev/null
+++ b/competencies/migrations/0001_initial.py
@@ -0,0 +1,81 @@
+# Generated by Django 3.1.13 on 2022-08-05 16:22
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Competency',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('updated', models.DateTimeField(auto_now=True)),
+ ('display_name', models.CharField(default='', max_length=255)),
+ ('is_visible', models.BooleanField(default=True)),
+ ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_competencies', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'Competency',
+ 'verbose_name_plural': 'Competencies',
+ },
+ ),
+ migrations.CreateModel(
+ name='CompetencyAssessment',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('updated', models.DateTimeField(auto_now=True)),
+ ('is_visible', models.BooleanField(default=True)),
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='competency_assessment', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'User Competency Self Assessment',
+ 'verbose_name_plural': 'User Competency Self Assessments',
+ },
+ ),
+ migrations.CreateModel(
+ name='CompetencyDifficulty',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('updated', models.DateTimeField(auto_now=True)),
+ ('display_name', models.CharField(default='', max_length=255, unique=True)),
+ ('numeric_difficulty', models.FloatField(default=1.0)),
+ ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_competency_difficulties', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'Competency Difficulty',
+ 'verbose_name_plural': 'Competency Difficulties',
+ },
+ ),
+ migrations.CreateModel(
+ name='CompetencyAssessmentRating',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('updated', models.DateTimeField(auto_now=True)),
+ ('rating', models.CharField(blank=True, choices=[('want_to_know', 'Want to know about it'), ('learning', 'Learning about it right now'), ('know_it', 'I know about this')], default='', max_length=50, null=True)),
+ ('competency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='competency_assessment_ratings', to='competencies.competency')),
+ ('user_assessment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='competencies', to='competencies.competencyassessment')),
+ ],
+ options={
+ 'verbose_name': 'Competency Self Assessment',
+ 'verbose_name_plural': 'Competency Self Assessments',
+ },
+ ),
+ migrations.AddField(
+ model_name='competency',
+ name='perceived_difficulty',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='competencies', to='competencies.competencydifficulty'),
+ ),
+ ]
diff --git a/competencies/migrations/0002_auto_20220808_1029.py b/competencies/migrations/0002_auto_20220808_1029.py
new file mode 100644
index 00000000..26f7a05f
--- /dev/null
+++ b/competencies/migrations/0002_auto_20220808_1029.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.13 on 2022-08-08 10:29
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('competencies', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='competencyassessmentrating',
+ name='rating',
+ field=models.CharField(blank=True, choices=[('no_knowledge', 'I have no knowledge'), ('want_to_know', 'Want to know about it'), ('learning', 'Learning about it right now'), ('know_it', 'I know about this')], default='', max_length=50, null=True),
+ ),
+ ]
diff --git a/competencies/migrations/__init__.py b/competencies/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/competencies/models.py b/competencies/models.py
new file mode 100644
index 00000000..1fa881a8
--- /dev/null
+++ b/competencies/models.py
@@ -0,0 +1,99 @@
+from django.db import models
+from django.core.exceptions import ObjectDoesNotExist
+
+from accounts.models import CustomUser as User
+
+ASSESSMENT_RATING_CHOICES = [
+ ('no_knowledge', 'I have no knowledge'),
+ ('want_to_know', 'Want to know about it'),
+ ('learning', 'Learning about it right now'),
+ ('know_it', 'I know about this'),
+]
+
+
+class CompetencyDifficulty(models.Model):
+ """ A percieved difficulty level of a competency or skill """
+ created = models.DateTimeField(auto_now_add=True)
+ updated = models.DateTimeField(auto_now=True)
+ created_by = models.ForeignKey(User,
+ on_delete=models.CASCADE,
+ related_name="created_competency_difficulties")
+
+ display_name = models.CharField(default="", max_length=255, unique=True)
+ numeric_difficulty = models.FloatField(default=1.0)
+
+ def __str__(self):
+ return f'{self.display_name} - {self.numeric_difficulty}'
+
+ class Meta:
+ verbose_name = 'Competency Difficulty'
+ verbose_name_plural = 'Competency Difficulties'
+
+
+class Competency(models.Model):
+ """ A competency or skill that a user can make a self assessment for """
+ created = models.DateTimeField(auto_now_add=True)
+ updated = models.DateTimeField(auto_now=True)
+ created_by = models.ForeignKey(User,
+ on_delete=models.CASCADE,
+ related_name="created_competencies")
+
+ display_name = models.CharField(default="", max_length=255)
+ perceived_difficulty = models.ForeignKey(CompetencyDifficulty,
+ on_delete=models.CASCADE,
+ related_name="competencies")
+ is_visible = models.BooleanField(default=True)
+
+ def __str__(self):
+ return self.display_name
+
+ class Meta:
+ verbose_name = 'Competency'
+ verbose_name_plural = 'Competencies'
+
+ def get_user_rating(self, user):
+ try:
+ return self.competency_assessment_ratings.get(
+ user_assessment__user=user,
+ user_assessment__is_visible=True)
+ except ObjectDoesNotExist:
+ return
+
+
+class CompetencyAssessment(models.Model):
+ created = models.DateTimeField(auto_now_add=True)
+ updated = models.DateTimeField(auto_now=True)
+
+ user = models.OneToOneField(User,
+ on_delete=models.CASCADE,
+ related_name="competency_assessment")
+ is_visible = models.BooleanField(default=True)
+
+ def __str__(self):
+ return self.user.slack_display_name
+
+ class Meta:
+ verbose_name = 'Competency Self Assessment'
+ verbose_name_plural = 'Competency Self Assessments'
+
+
+class CompetencyAssessmentRating(models.Model):
+ created = models.DateTimeField(auto_now_add=True)
+ updated = models.DateTimeField(auto_now=True)
+
+ user_assessment = models.ForeignKey(CompetencyAssessment,
+ on_delete=models.CASCADE,
+ related_name="competencies")
+ competency = models.ForeignKey(Competency,
+ on_delete=models.CASCADE,
+ related_name="competency_assessment_ratings")
+ rating = models.CharField(
+ default="", max_length=50, choices=ASSESSMENT_RATING_CHOICES,
+ null=True, blank=True)
+
+ def __str__(self):
+ return f'{self.user_assessment} - {self.rating}'
+
+ class Meta:
+ verbose_name = 'Competency Self Assessment Rating'
+ verbose_name_plural = 'Competency Self Assessment Ratings'
diff --git a/competencies/templates/competencies_self_assessment.html b/competencies/templates/competencies_self_assessment.html
new file mode 100644
index 00000000..783f2f94
--- /dev/null
+++ b/competencies/templates/competencies_self_assessment.html
@@ -0,0 +1,103 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
Self-Assess Your Competencies
+
+
+
+ {% if competencies %}
+
+
+ {% else %}
+ No competencies to self-assess available.
+ {% endif %}
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/competencies/templates/competency_difficulty_form.html b/competencies/templates/competency_difficulty_form.html
new file mode 100644
index 00000000..7c189b8d
--- /dev/null
+++ b/competencies/templates/competency_difficulty_form.html
@@ -0,0 +1,63 @@
+{% load static %}
+
+
+
+
+ Create New Difficulty Rating for Compentency
+
+
+
+
+
+
+
+
+
+
+ {% include "includes/messages.html" %}
+
+
+
+
+
+
+
+
+
Create New Difficulty Rating for Compentency
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if messages %}
+
+ {% endif %}
+
+
+
+
+
+
diff --git a/competencies/templates/competency_form.html b/competencies/templates/competency_form.html
new file mode 100644
index 00000000..5ab87d1b
--- /dev/null
+++ b/competencies/templates/competency_form.html
@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+
+
+
Create New Compentency for Participant Self-Assessment
+
+
+
+
+
+
+
+
+{% url 'create_competency_difficulty' as create_competency_difficulty_url %}
+
+
+{% endblock %}
diff --git a/competencies/templates/list_competencies.html b/competencies/templates/list_competencies.html
new file mode 100644
index 00000000..c1f3ce4a
--- /dev/null
+++ b/competencies/templates/list_competencies.html
@@ -0,0 +1,73 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+
+
+
Create New Compentency for Participant Self-Assessment
+
+
+
+
Create Competency
+
+ {% if competencies %}
+
+
+
+
+ | Name |
+ Perceived Difficulty |
+ # Want To Know About It |
+ # Learning It |
+ # Know It |
+ Actions |
+
+
+
+ {% for competency in competencies %}
+
+ |
+ {{ competency.display_name }}
+ |
+
+ {{ competency.perceived_difficulty }}
+ |
+
+ n/a
+ |
+
+ n/a
+ |
+
+ n/a
+ |
+
+
+
+
+
+ |
+
+ {% endfor %}
+
+
+
+ {% else %}
+ No competencies to self-assess available.
+ {% endif %}
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/competencies/tests.py b/competencies/tests.py
new file mode 100644
index 00000000..fdcfd9f0
--- /dev/null
+++ b/competencies/tests.py
@@ -0,0 +1,88 @@
+from django.test import TestCase
+
+from accounts.models import CustomUser as User, Organisation
+from competencies.helpers import (
+ get_or_create_competency_assessment,
+ populate_competency_assessment_for_formset)
+from competencies.models import (
+ Competency, CompetencyDifficulty,
+ CompetencyAssessment, CompetencyAssessmentRating)
+
+
+class CompetencyAssessmentTest(TestCase):
+ def setUp(self):
+ self.organisation = Organisation.objects.create()
+ self.user = User.objects.create(
+ username="testuser",
+ slack_display_name="testuser",
+ )
+ self.user2 = User.objects.create(
+ username="testuser2",
+ slack_display_name="testuser2",
+ )
+
+ self.assessment = CompetencyAssessment.objects.create(
+ user=self.user2,
+ is_visible=False,
+ )
+
+ competency_difficulty = CompetencyDifficulty.objects.create(
+ created_by=self.user,
+ display_name='Easy',
+ )
+ self.competency = Competency.objects.create(
+ created_by=self.user,
+ display_name='HTML',
+ perceived_difficulty=competency_difficulty,
+ )
+ self.competency2 = Competency.objects.create(
+ created_by=self.user,
+ display_name='CSS',
+ perceived_difficulty=competency_difficulty,
+ )
+ CompetencyAssessmentRating.objects.create(
+ user_assessment=self.assessment,
+ competency=self.competency,
+ rating='know_it'
+ )
+
+ def test_get_or_create_competency_assessment(self):
+ data = {
+ 'user': self.user.id,
+ 'is_visible': True
+ }
+ competency_assessment = get_or_create_competency_assessment(data)
+ self.assertTrue(isinstance(competency_assessment,
+ CompetencyAssessment))
+
+ def test_get_or_create_competency_assessment_error(self):
+ data = {}
+ competency_assessment = get_or_create_competency_assessment(data)
+ self.assertIsNone(competency_assessment)
+
+ def test_get_or_create_competency_assessment_existing_assssment(self):
+ data = {'user': self.user2.id}
+ competency_assessment = get_or_create_competency_assessment(data)
+ self.assertEquals(self.assessment.id, competency_assessment.id)
+
+ def test_populate_competency_assessment_for_formset(self):
+ data = {
+ 'form-0-competency': 1,
+ 'form-1-competency': 2,
+ }
+ populate_competency_assessment_for_formset(self.assessment, data)
+ self.assertEquals(data['form-0-user_assessment'], 1)
+ self.assertEquals(data['form-1-user_assessment'], 1)
+
+ data['form-2-competency'] = 3
+ populate_competency_assessment_for_formset(self.assessment, data)
+ self.assertEquals(data['form-0-user_assessment'], 1)
+ self.assertEquals(data['form-1-user_assessment'], 1)
+ self.assertEquals(data['form-2-user_assessment'], 1)
+
+ def test_get_user_rating(self):
+ assessment = self.competency.get_user_rating(self.user2)
+ self.assertEquals(assessment.rating, 'know_it')
+
+ assessment = self.competency2.get_user_rating(self.user2)
+ self.assertIsNone(assessment)
diff --git a/competencies/urls.py b/competencies/urls.py
new file mode 100644
index 00000000..e4675e00
--- /dev/null
+++ b/competencies/urls.py
@@ -0,0 +1,17 @@
+from django.urls import path
+
+from competencies import views
+
+
+
+urlpatterns = [
+ path('', views.self_assess_competencies, name="self_assess_competencies"),
+ path('list', views.list_competencies, name="list_competencies"),
+ path('create', views.create_competency, name="create_competency"),
+ path('edit/', views.edit_competency,
+ name="edit_competency"),
+ path('difficulty/create', views.create_competency_difficulty,
+ name="create_competency_difficulty"),
+ path('difficulty/edit/',
+ views.edit_competency_difficulty, name="edit_competency_difficulty"),
+]
diff --git a/competencies/views.py b/competencies/views.py
new file mode 100644
index 00000000..c3bdeda8
--- /dev/null
+++ b/competencies/views.py
@@ -0,0 +1,208 @@
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import ObjectDoesNotExist
+from django.forms import modelformset_factory
+from django.shortcuts import get_object_or_404, redirect, render, reverse
+
+from accounts.models import UserType
+from accounts.decorators import can_access
+from competencies.forms import (
+ CompetencyForm, CompetencyDifficultyForm,
+ CompetencyAssessmentForm,
+ CompetencyAssessmentRatingForm,
+ RequiredModelFormSet)
+
+from competencies.helpers import (
+ get_or_create_competency_assessment,
+ populate_competency_assessment_for_formset)
+
+from competencies.models import (
+ Competency, CompetencyDifficulty,
+ CompetencyAssessment, CompetencyAssessmentRating)
+
+
+@login_required
+@can_access([UserType.SUPERUSER, UserType.STAFF, UserType.FACILITATOR_ADMIN,
+ UserType.PARTNER_ADMIN],
+ redirect_url='hackathon:hackathon-list')
+def list_competencies(request):
+ competencies = Competency.objects.order_by('display_name')
+ return render(request, 'list_competencies.html', {'competencies': competencies})
+
+
+@login_required
+@can_access([UserType.SUPERUSER, UserType.STAFF, UserType.FACILITATOR_ADMIN,
+ UserType.PARTNER_ADMIN],
+ redirect_url='hackathon:hackathon-list')
+def create_competency_difficulty(request):
+ if request.method == 'POST':
+ form = CompetencyDifficultyForm(request.POST)
+ if form.is_valid():
+ f = form.save(commit=False)
+ f.created_by = request.user
+ f.save()
+ messages.success(request, "Competency Difficulty created successfully.")
+ url = "%s?close_popup=true" % reverse('create_competency_difficulty')
+ return redirect(url)
+ else:
+ messages.error(request, form.errors)
+ return redirect(reverse('create_competency_difficulty'))
+ else:
+ form = CompetencyDifficultyForm()
+ return render(request, 'competency_difficulty_form.html', {'form': form})
+
+
+@login_required
+@can_access([UserType.SUPERUSER, UserType.STAFF, UserType.FACILITATOR_ADMIN,
+ UserType.PARTNER_ADMIN],
+ redirect_url='hackathon:hackathon-list')
+def edit_competency_difficulty(request, competency_difficulty_id):
+ competency_difficulty = get_object_or_404(CompetencyDifficulty,
+ id=competency_difficulty_id)
+ if request.method == 'POST':
+ form = CompetencyDifficultyForm(request.POST, instance=competency_difficulty)
+ if form.is_valid():
+ f = form.save(commit=False)
+ f.created_by = request.user
+ f.save()
+ messages.success(request, "Competency Difficulty edited successfully.")
+ url = "%s?close_popup=true" % reverse('create_competency_difficulty')
+ return redirect(url)
+ else:
+ messages.error(request, form.errors)
+ return redirect(reverse('edit_competency_difficulty'))
+ else:
+ form = CompetencyDifficultyForm(instance=competency_difficulty)
+ return render(request, 'competency_difficulty_form.html', {'form': form})
+
+
+@login_required
+@can_access([UserType.SUPERUSER, UserType.STAFF, UserType.FACILITATOR_ADMIN,
+ UserType.PARTNER_ADMIN],
+ redirect_url='hackathon:hackathon-list')
+def create_competency(request):
+ if request.method == 'POST':
+ form = CompetencyForm(request.POST)
+ if form.is_valid():
+ f = form.save(commit=False)
+ f.created_by = request.user
+ f.save()
+ messages.success(request, "Competency created successfully.")
+ return redirect(reverse('list_competencies'))
+ else:
+ messages.error(request, form.errors)
+ return redirect(reverse('create_competency'))
+ else:
+ form = CompetencyForm()
+ return render(request, 'competency_form.html', {'form': form})
+
+
+@login_required
+@can_access([UserType.SUPERUSER, UserType.STAFF, UserType.FACILITATOR_ADMIN,
+ UserType.PARTNER_ADMIN],
+ redirect_url='hackathon:hackathon-list')
+def edit_competency(request, competency_id):
+ competency = get_object_or_404(Competency,
+ id=competency_id)
+ if request.method == 'POST':
+ form = CompetencyForm(request.POST, instance=competency)
+ if form.is_valid():
+ f = form.save(commit=False)
+ f.created_by = request.user
+ f.save()
+ messages.success(request, "Competency edited successfully.")
+ return redirect(reverse('list_competencies'))
+ else:
+ messages.error(request, form.errors)
+ return redirect(reverse('edit_competency'))
+ else:
+ form = CompetencyForm(instance=competency)
+ return render(request, 'competency_form.html', {'form': form})
+
+@login_required
+def self_assess_competencies(request):
+ data = request.POST.copy()
+ competencies = Competency.objects.filter(is_visible=True)
+ try:
+ competency_assessment = CompetencyAssessment.objects.get(
+ user=request.user)
+ except ObjectDoesNotExist:
+ competency_assessment = None
+
+ if request.method == 'POST':
+ CompetencyAssessmentRatingFormSet = modelformset_factory(
+ CompetencyAssessmentRating, fields=(
+ 'user_assessment', 'competency', 'rating'),
+ form=CompetencyAssessmentRatingForm,
+ formset=RequiredModelFormSet,
+ )
+ anything_filled_in = any(key.endswith('-rating')
+ for key in data.keys())
+ if not anything_filled_in:
+ messages.error(request, 'Nothing selected.')
+ return redirect(reverse('self_assess_competencies'))
+
+ competency_assessment = get_or_create_competency_assessment(data)
+ if not competency_assessment:
+ messages.error(request, form.errors)
+ return redirect(reverse('self_assess_competencies'))
+
+ populate_competency_assessment_for_formset(competency_assessment, data)
+ if competency_assessment:
+ competency_assessments=competency_assessment.competencies.all()
+ else:
+ competency_assessments=CompetencyAssessmentRating.objects.none()
+ formset = CompetencyAssessmentRatingFormSet(data,
+ queryset=competency_assessments)
+
+ if formset.is_valid():
+ formset.save()
+ else:
+ messages.error(request, "Errors in the information")
+ return redirect(reverse('self_assess_competencies'))
+ return redirect(reverse('self_assess_competencies'))
+ else:
+ initial=[]
+ competency_assessments=CompetencyAssessmentRating.objects.none()
+ if competency_assessment:
+ assessment_competencies = competency_assessment.competencies
+ if assessment_competencies.count() < competencies.count():
+ competency_ids = [competency.id for competency in competencies]
+ exclude_ids = [competency.id
+ for competency in assessment_competencies.all()
+ if competency.id not in competency_ids]
+ competency_assessments = assessment_competencies.exclude(
+ id__in=exclude_ids)
+ initial = [{'competency': competency}
+ for competency in competencies.all()
+ if competency.id not in [
+ c.id for c in competency_assessments]]
+ else:
+ competency_assessments = assessment_competencies.all()
+ else:
+ initial = [{'competency': competency}
+ for competency in competencies.all()]
+
+ CompetencyAssessmentRatingFormSet = modelformset_factory(
+ CompetencyAssessmentRating, fields=(
+ 'user_assessment', 'competency', 'rating'),
+ form=CompetencyAssessmentRatingForm,
+ formset=RequiredModelFormSet,
+ extra=len(initial),
+ )
+
+ formset = CompetencyAssessmentRatingFormSet(
+ queryset=competency_assessments,
+ initial=initial,
+ )
+
+ if competency_assessment:
+ form = CompetencyAssessmentForm(instance=competency_assessment)
+ else:
+ form = CompetencyAssessmentForm(initial={
+ 'user': request.user, 'is_visible': True})
+
+ return render(request, 'competencies_self_assessment.html', {
+ 'form': form, 'competencies': competencies,
+ 'formset': formset,
+})
diff --git a/docker-compose.yml b/docker-compose.yml
index 54136f36..e401b719 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,6 +10,7 @@ services:
# code
- ./accounts/:/hackathon-app/accounts/
+ - ./competencies/:/hackathon-app/competencies/
- ./custom_slack_provider/:/hackathon-app/custom_slack_provider/
- ./hackadmin/:/hackathon-app/hackadmin/
- ./hackathon/:/hackathon-app/hackathon/
@@ -26,6 +27,7 @@ services:
environment:
- ENV_FILE=/hackathon-app/.env
+ - DEVELOPMENT=1
entrypoint: ['python3', 'manage.py', 'runserver', '0.0.0.0:8000']
ports:
- "8000:8000"
@@ -45,4 +47,4 @@ services:
smtp:
image: mailhog/mailhog:v1.0.1
ports:
- - "8025:8025"
+ - "8026:8025"
diff --git a/hackadmin/templates/hackadmin_panel.html b/hackadmin/templates/hackadmin_panel.html
index bc5cae83..498ab466 100644
--- a/hackadmin/templates/hackadmin_panel.html
+++ b/hackadmin/templates/hackadmin_panel.html
@@ -10,7 +10,8 @@
Hack Admin
{% if hackathons %}
diff --git a/main/settings.py b/main/settings.py
index 0a7a204f..5268a5c2 100644
--- a/main/settings.py
+++ b/main/settings.py
@@ -39,6 +39,7 @@
# custom apps
"accounts",
+ "competencies",
"hackathon",
"hackadmin",
"home",
diff --git a/main/urls.py b/main/urls.py
index 39719253..49329401 100644
--- a/main/urls.py
+++ b/main/urls.py
@@ -16,4 +16,5 @@
namespace='hackathon')),
path("showcase/", include("showcase.urls")),
path("teams/", include("teams.urls")),
+ path("competencies/", include("competencies.urls")),
]
diff --git a/static/css/style.css b/static/css/style.css
index c9e5f1b2..95ec6809 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -31,6 +31,10 @@ body {
height: 100%;
}
+html form.w-100 {
+ max-width: 100%;
+}
+
body * {
font-family: Montserrat, sans-serif;
}
@@ -811,6 +815,72 @@ img.showcase-image-edit {
display:none;
}
+/* Competencies */
+.input-group.competency-difficulty-select select {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.input-group.competency-difficulty-select a.btn {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ margin: 0;
+ font-size: 1rem;
+}
+
+.card .card-body a.btn.btn-large {
+ font-size: .95rem;
+}
+
+#competenciesTable.table td {
+ vertical-align: middle;
+}
+
+#competenciesTable.table td label {
+ margin: 0;
+}
+
+#competenciesTable i {
+ cursor: pointer;
+}
+
+#competenciesDisplayTable i.fas,
+#competenciesDisplayTable i.fas:hover {
+ transform: scale(1);
+ cursor: default;
+ vertical-align: middle;
+}
+
+#competenciesTable i,
+i.rating-display {
+ font-size: 1.3rem;
+}
+
+#competenciesTable input[type="radio"] {
+ visibility: hidden;
+}
+
+i.fas.fa-arrow-alt-circle-up {
+ color: var(--s-teal);
+}
+
+i.fas.fa-lightbulb {
+ color: var(--s-yellow);
+}
+
+i.fas.fa-thumbs-up {
+ color: var(--p-blue);
+}
+
+.disabled-select {
+ pointer-events: none;
+ border: 0;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ color: var(--black);
+ padding: 0;
+}
+
/* Media Queries */
@media (max-width:453px) {
diff --git a/static/js/script.js b/static/js/script.js
index 60f1c5e7..c80e195a 100644
--- a/static/js/script.js
+++ b/static/js/script.js
@@ -26,6 +26,9 @@ $(document).ready(function(){
$(`#hackadmin-team-select-${hackathonId}`).show();
});
enableReviewsSlider();
+ openCompetencyDifficultyInPopup();
+ closePopup();
+ toggleCompetencyAssessmentIcon();
});
function setUpoadImageType(){
@@ -129,3 +132,49 @@ function enableReviewsSlider(){
}
});
}
+
+
+function openCompetencyDifficultyInPopup(){
+ $('#openCompetencyDifficultyPopup').click(function(){
+ const params = `width=500,height=350,left=-1000,top=-1000`;
+ const window_name = 'Create Competency Difficulty';
+ window.open(create_competency_difficulty_url, window_name, params);
+ })
+}
+
+function closePopup(){
+ let queryString = window.location.search;
+ let urlParams = new URLSearchParams(queryString);
+ let close_popup = urlParams.get('close_popup');
+ if(close_popup){
+ window.opener.location.reload();
+ window.close();
+ }
+}
+
+function toggleCompetencyAssessmentIcon() {
+ $('.competency-assessment-radio').change(function(){
+ let current_selection = $(this).parent().parent().parent().find('label i.fas');
+ if(current_selection.length > 0){
+ _changeClass(current_selection[0]);
+ }
+ let new_selection = $(this).parent().find('label i');
+ _changeClass(new_selection[0]);
+ _chageSelection($(this).data('form'), $(this).data('rating'));
+ });
+}
+
+function _changeClass(element){
+ if(element.classList.contains('fas')){
+ element.classList.remove('fas');
+ element.classList.add('far');
+ } else {
+ element.classList.remove('far');
+ element.classList.add('fas');
+ }
+}
+
+
+function _chageSelection(form_num, rating){
+ $(`#id_form-${form_num}-rating`).val(rating);
+}
diff --git a/teams/templates/team.html b/teams/templates/team.html
index 5ce0fc3b..1540c46c 100644
--- a/teams/templates/team.html
+++ b/teams/templates/team.html
@@ -64,6 +64,9 @@
About the team
View Team Calendar
+
+ View Competencies
+
diff --git a/teams/templates/team_competencies.html b/teams/templates/team_competencies.html
new file mode 100644
index 00000000..31afd9df
--- /dev/null
+++ b/teams/templates/team_competencies.html
@@ -0,0 +1,82 @@
+{% extends "base.html" %}
+{% load static %}
+{% load teams_tags %}
+{% load account_tags %}
+
+{% block css %}
+
+{% endblock %}
+
+{% block content %}
+
+{% with authorised_types='SUPERUSER,STAFF,FACILITATOR_ADMIN,FACILITATOR_JUDGE,PARTNER_ADMIN,PARTNER_JUDGE' %}
+
+
+
+
+
+
+
+ {% include 'includes/back_button.html' with redirect_url=redirect_url button_label='Back To The Team' %}
+
+
+
+
Our Hack Team Competencies & Experience
+
+
+
+
+
+
+
+
+
+ |
+ Competency
+ |
+ {% for participant in hack_team.participants.all %}
+
+ {{participant.slack_display_name}}
+ |
+ {% endfor %}
+
+
+
+ {% for comp in competencies %}
+
+ |
+ {{comp.display_name}}
+ |
+ {% for participant in hack_team.participants.all %}
+
+ {% get_participant_rating participant comp as level %}
+ {% if level == 'know_it' %}
+
+ {% elif level == 'want_to_know' %}
+
+ {% elif level == 'learning' %}
+
+ {% else %}
+
+ {% endif %}
+ |
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+{% endwith %}
+
+{% endblock %}
+
+{% block js %}
+
+{% endblock %}
diff --git a/teams/templatetags/teams_tags.py b/teams/templatetags/teams_tags.py
index 9b76ae22..ec9b2aae 100644
--- a/teams/templatetags/teams_tags.py
+++ b/teams/templatetags/teams_tags.py
@@ -3,6 +3,7 @@
from django.template import Library
from django.conf import settings
+from django.core.exceptions import ObjectDoesNotExist
register = Library()
@@ -45,3 +46,11 @@ def extract_userid(username):
def is_working_time(num):
_num = int(num.split(':')[0])
return 8 <= _num <= 20
+
+
+@register.simple_tag
+def get_participant_rating(participant, competency):
+ try:
+ return competency.get_user_rating(participant).rating
+ except (ObjectDoesNotExist, AttributeError):
+ return
diff --git a/teams/urls.py b/teams/urls.py
index 301c653b..741f820e 100644
--- a/teams/urls.py
+++ b/teams/urls.py
@@ -1,20 +1,22 @@
from django.urls import path
-from .views import view_team, create_teams, clear_teams, create_project,\
- rename_team, create_group_im, view_team_calendar
+from . import views
from showcase.views import create_or_update_showcase
urlpatterns = [
- path("/", view_team,
+ path("/", views.view_team,
name="view_team"),
- path("/project/", create_project, name="create_project"),
+ path("/project/", views.create_project,
+ name="create_project"),
path("/showcase/",
create_or_update_showcase, name="create_or_update_showcase"),
- path("/create_group_im/", create_group_im,
+ path("/create_group_im/", views.create_group_im,
name="create_group_im"),
- path("/rename/", rename_team, name="rename_team"),
- path("/calendar/", view_team_calendar,
+ path("/rename/", views.rename_team, name="rename_team"),
+ path("/calendar/", views.view_team_calendar,
name="view_team_calendar"),
- path("create/", create_teams, name="create_teams"),
- path("clear/", clear_teams, name="clear_teams"),
+ path("/competencies/", views.view_team_competencies,
+ name="view_team_competencies"),
+ path("create/", views.create_teams, name="create_teams"),
+ path("clear/", views.clear_teams, name="clear_teams"),
]
diff --git a/teams/views.py b/teams/views.py
index e654f292..0b5b5e06 100644
--- a/teams/views.py
+++ b/teams/views.py
@@ -13,12 +13,14 @@
from accounts.decorators import can_access
from accounts.models import UserType
from accounts.lists import TIMEZONE_CHOICES
+from competencies.models import Competency
from hackathon.models import Hackathon, HackTeam, HackProject
-from teams.helpers import choose_team_sizes, group_participants,\
- choose_team_levels, find_all_combinations,\
- distribute_participants_to_teams,\
- create_teams_in_view, update_team_participants, \
- calculate_timezone_offset
+from teams.helpers import (
+ choose_team_sizes, group_participants,
+ choose_team_levels, find_all_combinations,
+ distribute_participants_to_teams,
+ create_teams_in_view, update_team_participants,
+ calculate_timezone_offset)
from teams.forms import HackProjectForm, EditTeamName
SLACK_GROUP_IM_ENDPOINT = 'https://slack.com/api/conversations.open/'
@@ -321,3 +323,15 @@ def view_team_calendar(request, team_id):
'timezones': TIMEZONE_CHOICES,
'selected_timezone': timezone,
})
+
+
+@login_required
+def view_team_competencies(request, team_id):
+ hack_team = get_object_or_404(HackTeam, id=team_id)
+ competencies = Competency.objects.filter(is_visible=True)
+ redirect_url = reverse('view_team', kwargs={'team_id': team_id})
+ return render(request, 'team_competencies.html', {
+ 'hack_team': hack_team,
+ 'competencies': competencies,
+ 'redirect_url': redirect_url,
+ })
diff --git a/templates/includes/navbar.html b/templates/includes/navbar.html
index ac19cd2f..e8380b4b 100644
--- a/templates/includes/navbar.html
+++ b/templates/includes/navbar.html
@@ -65,6 +65,7 @@
Login
{% else %}
My Profile
+ My Competencies
{% if request.user.user_type|is_type:"SUPERUSER" %}
Django Admin
{% endif %}