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 %} +
+ {% csrf_token %} + {{form.user.as_hidden}} + {{form.is_visible|as_crispy_field}} + {{ formset.management_form }} +
+ + + + + + + + + + + + {% for form in formset %} + + + + + + + + + + + {% endfor %} +
CompetencyNo KnowledgeWant to LearnLearning it nowKnow it
+ {{form.id}} + {{form.user_assessment.as_hidden}} + {{form.rating.as_hidden}} + {{form.competency}} + + +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ Back + +
+ + {% 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

+
+
+ +
+ {% csrf_token %} + {{ form.display_name|as_crispy_field }} + {{ form.numeric_difficulty|as_crispy_field }} + +
+ +
+
+
+ +
+
+
+ {% 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

+
+
+ +
+ {% csrf_token%} + {{form.display_name|as_crispy_field}} + +
+ {{form.perceived_difficulty}} + + + +
+ Back + +
+ +
+
+
+ +
+
+ +{% 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 %} +
+ + + + + + + + + + + + + {% for competency in competencies %} + + + + + + + + + {% endfor %} +
NamePerceived Difficulty# Want To Know About It# Learning It# Know ItActions
+ {{ competency.display_name }} + + {{ competency.perceived_difficulty }} + + n/a + + n/a + + n/a + + + + + +
+
+ + {% 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
- View All Users +

View All Users

+

View All Competencies

{% 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

+
+
+ +
+
+ + + + + + {% for participant in hack_team.participants.all %} + + {% endfor %} + + + + {% for comp in competencies %} + + + {% for participant in hack_team.participants.all %} + + {% endfor %} + + {% endfor %} + +
+ Competency + + {{participant.slack_display_name}} +
+ {{comp.display_name}} + + {% get_participant_rating participant comp as level %} + {% if level == 'know_it' %} + + {% elif level == 'want_to_know' %} + + {% elif level == 'learning' %} + + {% else %} +   + {% endif %} +
+ +
+
+ +
+ +
+
+{% 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 %}