From d4b144d2cbcea260f18808e3dffe548d9ac53bc8 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Thu, 4 Nov 2021 11:40:42 -0400 Subject: [PATCH 01/14] positions: Create position models and add app Per #562, this creates a new django app for the position postings. The thought process behind the position flow is: 1. Exec posts a position 2. Active members can see that there are open positions, and can view them on the DB and apply with 1 click (side thought on displaying -- I was thinking something similar to the CC report reminders but on the main page) 3. Applicants are given a form to complete if provided by exec, and exec can see the applicants event/training history in the position view 4. Exec votes and does exec-y things to pick the person, who then gets a congratulatory email and the position is marked as closed. Writing this out, I need to add a field to the `Position` model to mark if the position has been filled. --- lnldb/settings.py | 1 + positions/__init__.py | 0 positions/admin.py | 13 +++++++ positions/apps.py | 6 +++ positions/migrations/0001_initial.py | 28 ++++++++++++++ .../migrations/0002_auto_20211104_1125.py | 37 +++++++++++++++++++ positions/migrations/__init__.py | 0 positions/models.py | 37 +++++++++++++++++++ positions/tests.py | 3 ++ positions/views.py | 3 ++ 10 files changed, 128 insertions(+) create mode 100644 positions/__init__.py create mode 100644 positions/admin.py create mode 100644 positions/apps.py create mode 100644 positions/migrations/0001_initial.py create mode 100644 positions/migrations/0002_auto_20211104_1125.py create mode 100644 positions/migrations/__init__.py create mode 100644 positions/models.py create mode 100644 positions/tests.py create mode 100644 positions/views.py diff --git a/lnldb/settings.py b/lnldb/settings.py index 4f4252b7..1882d6c4 100644 --- a/lnldb/settings.py +++ b/lnldb/settings.py @@ -322,6 +322,7 @@ def from_runtime(*x): 'api', 'rt', 'slack', + 'positions', 'bootstrap3', 'crispy_forms', diff --git a/positions/__init__.py b/positions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/positions/admin.py b/positions/admin.py new file mode 100644 index 00000000..a7e83e2a --- /dev/null +++ b/positions/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin + +from .models import Position + +# Register your models here. + +class PositionAdmin(admin.ModelAdmin): + fields = ( + 'name', + 'description' + ) + +admin.site.register(Position, PositionAdmin) diff --git a/positions/apps.py b/positions/apps.py new file mode 100644 index 00000000..490dbf89 --- /dev/null +++ b/positions/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PositionsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'positions' diff --git a/positions/migrations/0001_initial.py b/positions/migrations/0001_initial.py new file mode 100644 index 00000000..0bdfd865 --- /dev/null +++ b/positions/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.13 on 2021-11-03 20:49 + +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='Position', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32, verbose_name='Position Name')), + ('description', models.TextField(verbose_name='Position Description')), + ('position_start', models.DateField(verbose_name='Term Start')), + ('position_end', models.DateField(verbose_name='Term End')), + ('reports_to', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/positions/migrations/0002_auto_20211104_1125.py b/positions/migrations/0002_auto_20211104_1125.py new file mode 100644 index 00000000..03ea2a30 --- /dev/null +++ b/positions/migrations/0002_auto_20211104_1125.py @@ -0,0 +1,37 @@ +# Generated by Django 3.1.13 on 2021-11-04 15:25 + +import datetime +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), + ('positions', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='position', + name='application_form', + field=models.CharField(default='', max_length=128, verbose_name='Link to external application form'), + preserve_default=False, + ), + migrations.AddField( + model_name='position', + name='closes', + field=models.DateTimeField(default=datetime.datetime.now, verbose_name='Applications Close'), + preserve_default=False, + ), + migrations.CreateModel( + name='ApplicationInstance', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('applicant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('position', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='positions.position')), + ], + ), + ] diff --git a/positions/migrations/__init__.py b/positions/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/positions/models.py b/positions/models.py new file mode 100644 index 00000000..6ed3fa3b --- /dev/null +++ b/positions/models.py @@ -0,0 +1,37 @@ +from django.db import models +from django.contrib.auth import get_user_model + +import datetime + +# Create your models here. + +class Position(models.Model): + """ + Describes a leadership position for a specific time. A new position instance + should be created every time one needs to be filled. + """ + name = models.CharField(verbose_name="Position Name", max_length=32, + null=False, blank=False) + description = models.TextField(verbose_name="Position Description", + null=False, blank=False) + position_start = models.DateField(verbose_name="Term Start", null=False, + blank=False) + position_end = models.DateField(verbose_name="Term End", null=False, + blank=False) + reports_to = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + closes = models.DateTimeField(verbose_name="Applications Close", null=False, + blank=False) + application_form = models.CharField(verbose_name="Link to external application form", null=False, blank=False, max_length=128) + + def __str__(self): + return f"{self.name}" + + def is_open(self): + return self.closes >= datetime.datetime.now() + +class ApplicationInstance(models.Model): + """ + Defines a specific application instance for a user and position. + """ + applicant = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + position = models.ForeignKey(Position, on_delete=models.CASCADE) diff --git a/positions/tests.py b/positions/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/positions/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/positions/views.py b/positions/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/positions/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 6023874696b97eb6101765a7deca13204ab6a16d Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Thu, 11 Nov 2021 14:15:01 -0500 Subject: [PATCH 02/14] positions: Add basic position templates This commit will add the following templates: - position_list - position_detail It will update the following models: - Position I'm debating just getting rid of the ApplicationInstance model entirely and letting exec track applications through their own external tools. --- lnldb/urls.py | 1 + positions/admin.py | 8 +--- .../migrations/0003_auto_20211111_1309.py | 23 +++++++++++ positions/models.py | 6 +-- positions/urls.py | 11 +++++ positions/views.py | 25 ++++++++++++ site_tmpl/admin.nav.html | 5 +++ site_tmpl/position_detail.html | 40 +++++++++++++++++++ site_tmpl/position_list.html | 29 ++++++++++++++ 9 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 positions/migrations/0003_auto_20211111_1309.py create mode 100644 positions/urls.py create mode 100644 site_tmpl/position_detail.html create mode 100644 site_tmpl/position_list.html diff --git a/lnldb/urls.py b/lnldb/urls.py index 68594005..fe4c1180 100644 --- a/lnldb/urls.py +++ b/lnldb/urls.py @@ -54,6 +54,7 @@ url(r'^mdm/', include(('devices.urls.mdm', 'mdm'), namespace="mdm")), url(r'^support/', include(('rt.urls', 'support'), namespace='support')), url(r'', include(('pages.urls', 'pages'), namespace='pages')), + url(r'^db/positions/', include('positions.urls', 'positions')), # special urls url(r'^db/$', db_home, name="home"), diff --git a/positions/admin.py b/positions/admin.py index a7e83e2a..d895a901 100644 --- a/positions/admin.py +++ b/positions/admin.py @@ -4,10 +4,4 @@ # Register your models here. -class PositionAdmin(admin.ModelAdmin): - fields = ( - 'name', - 'description' - ) - -admin.site.register(Position, PositionAdmin) +admin.site.register(Position) diff --git a/positions/migrations/0003_auto_20211111_1309.py b/positions/migrations/0003_auto_20211111_1309.py new file mode 100644 index 00000000..2e93d925 --- /dev/null +++ b/positions/migrations/0003_auto_20211111_1309.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.13 on 2021-11-11 18:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('positions', '0002_auto_20211104_1125'), + ] + + operations = [ + migrations.AlterField( + model_name='position', + name='closes', + field=models.DateField(blank=True, null=True, verbose_name='Applications Close'), + ), + migrations.AlterField( + model_name='position', + name='name', + field=models.CharField(max_length=64, verbose_name='Position Name'), + ), + ] diff --git a/positions/models.py b/positions/models.py index 6ed3fa3b..30f9abdd 100644 --- a/positions/models.py +++ b/positions/models.py @@ -10,7 +10,7 @@ class Position(models.Model): Describes a leadership position for a specific time. A new position instance should be created every time one needs to be filled. """ - name = models.CharField(verbose_name="Position Name", max_length=32, + name = models.CharField(verbose_name="Position Name", max_length=64, null=False, blank=False) description = models.TextField(verbose_name="Position Description", null=False, blank=False) @@ -19,8 +19,8 @@ class Position(models.Model): position_end = models.DateField(verbose_name="Term End", null=False, blank=False) reports_to = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) - closes = models.DateTimeField(verbose_name="Applications Close", null=False, - blank=False) + closes = models.DateField(verbose_name="Applications Close", null=True, + blank=True) application_form = models.CharField(verbose_name="Link to external application form", null=False, blank=False, max_length=128) def __str__(self): diff --git a/positions/urls.py b/positions/urls.py new file mode 100644 index 00000000..6569b863 --- /dev/null +++ b/positions/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from . import views + +app_name="positions" + +urlpatterns = [ + path('create/', views.CreatePosition.as_view(), name="create"), + path('list/', views.ListPositions.as_view(), name="list"), + path('detail/', views.ViewPosition.as_view(), name="detail"), + ] diff --git a/positions/views.py b/positions/views.py index 91ea44a2..d6eecab9 100644 --- a/positions/views.py +++ b/positions/views.py @@ -1,3 +1,28 @@ from django.shortcuts import render +from django.views import generic +from django.urls import reverse + +from .models import Position # Create your views here. + +class CreatePosition(generic.CreateView): + model = Position + fields = ('name', 'description', 'position_start', 'position_end', + 'reports_to', 'closes', 'application_form') + template_name="form_crispy_cbv.html" + permission_required="positions.add_position" + + def get_success_url(self): + return reverse("accounts:me") + +class ListPositions(generic.ListView): + model = Position + fields = ('name', 'position_start', 'position_end', 'closes') + template_name = 'position_list.html' + permission_required = "positions.apply" + +class ViewPosition(generic.DetailView): + model = Position + template_name = "position_detail.html" + permission_required = "positions.apply" diff --git a/site_tmpl/admin.nav.html b/site_tmpl/admin.nav.html index 8be52e1d..2b000aef 100644 --- a/site_tmpl/admin.nav.html +++ b/site_tmpl/admin.nav.html @@ -120,6 +120,11 @@
  • Send Message...
  • {% endpermission %} + {% permission request.user has 'positions.apply' or request.user has 'positions.add_position' %} +
  • Open + Positions
  • +
  • + {% endpermission %} {% permission request.user has 'events.view_attendance_records' %}
  • Crew Logs
  • {% endpermission %} diff --git a/site_tmpl/position_detail.html b/site_tmpl/position_detail.html new file mode 100644 index 00000000..035bbf2d --- /dev/null +++ b/site_tmpl/position_detail.html @@ -0,0 +1,40 @@ +{% extends 'base_admin.html' %} +{% load permissionif %} + +{% block content %} +
    +
    +

    {{ object }}

    +

    Term: {{ object.position_start}} through {{object.position_end}}

    +
    + {% permission user has 'positions.change_position' %} + Edit + {% endpermission %} + Apply +
    + + + + + + + + + + + + + + + + + + + + + + +
    Description{{ object.description }}
    Term Start{{ object.position_start }}
    Term End{{ object.position_end }}
    Reports To{{ object.reports_to }}
    Applications Close{% if object.closes %}{{ object.closes }}{% else %}Doesn't + close{% endif %}
    + +{% endblock %} diff --git a/site_tmpl/position_list.html b/site_tmpl/position_list.html new file mode 100644 index 00000000..e7344305 --- /dev/null +++ b/site_tmpl/position_list.html @@ -0,0 +1,29 @@ +{% extends 'base_admin.html' %} +{% load bootstrap3 %} + +{% block content %} +

    Open positions

    +

    These are positions that the executive board is seeking applications for. +Click on a position name to view more details.

    + + + + + + + + + + + {% for pos in object_list %} + + + + + + +{% endfor %} + +
    NameStart dateEnd DateApplication Closes
    {{ pos }}{{ pos.position_start }}{{ pos.position_end }}{% if pos.closes %}{{ pos.closes }}{% else %}Does not + close{% endif %}
    +{% endblock %} From 4bc0b6a7123c79108eb42d77b4e21a78a450e284 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Thu, 11 Nov 2021 14:19:44 -0500 Subject: [PATCH 03/14] positions: Make reports_to field optional Some positions won't have any reports (e.g exec board positions posted so that potential candidates know what the job entails). --- .../migrations/0004_auto_20211111_1419.py | 21 +++++++++++++++++++ positions/models.py | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 positions/migrations/0004_auto_20211111_1419.py diff --git a/positions/migrations/0004_auto_20211111_1419.py b/positions/migrations/0004_auto_20211111_1419.py new file mode 100644 index 00000000..88b948b6 --- /dev/null +++ b/positions/migrations/0004_auto_20211111_1419.py @@ -0,0 +1,21 @@ +# Generated by Django 3.1.13 on 2021-11-11 19:19 + +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), + ('positions', '0003_auto_20211111_1309'), + ] + + operations = [ + migrations.AlterField( + model_name='position', + name='reports_to', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/positions/models.py b/positions/models.py index 30f9abdd..ad2fed28 100644 --- a/positions/models.py +++ b/positions/models.py @@ -18,7 +18,8 @@ class Position(models.Model): blank=False) position_end = models.DateField(verbose_name="Term End", null=False, blank=False) - reports_to = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + reports_to = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, + blank=True, null=True) closes = models.DateField(verbose_name="Applications Close", null=True, blank=True) application_form = models.CharField(verbose_name="Link to external application form", null=False, blank=False, max_length=128) From 2689b2a5fb75b6f0c22786a1a7dd9f14c2a169a9 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Fri, 12 Nov 2021 11:57:23 -0500 Subject: [PATCH 04/14] positions: Enable editing and creation of new positions --- positions/forms.py | 14 ++++++++++++++ positions/urls.py | 1 + positions/views.py | 23 ++++++++++++++++++----- site_tmpl/position_detail.html | 2 +- 4 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 positions/forms.py diff --git a/positions/forms.py b/positions/forms.py new file mode 100644 index 00000000..85b6b30b --- /dev/null +++ b/positions/forms.py @@ -0,0 +1,14 @@ +from django import forms +from .models import Position +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Submit + +class UpdateCreatePosition(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.add_input(Submit('submit', 'Submit')) + class Meta: + model = Position + fields = ('name', 'description', 'position_start', 'position_end', + 'closes', 'reports_to', 'application_form') diff --git a/positions/urls.py b/positions/urls.py index 6569b863..bb3aa187 100644 --- a/positions/urls.py +++ b/positions/urls.py @@ -8,4 +8,5 @@ path('create/', views.CreatePosition.as_view(), name="create"), path('list/', views.ListPositions.as_view(), name="list"), path('detail/', views.ViewPosition.as_view(), name="detail"), + path('detail//edit', views.UpdatePosition.as_view(), name="update"), ] diff --git a/positions/views.py b/positions/views.py index d6eecab9..c0234b2c 100644 --- a/positions/views.py +++ b/positions/views.py @@ -1,28 +1,41 @@ from django.shortcuts import render from django.views import generic from django.urls import reverse +import datetime from .models import Position +from .forms import UpdateCreatePosition + +from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin # Create your views here. -class CreatePosition(generic.CreateView): +class CreatePosition(generic.CreateView, PermissionRequiredMixin, LoginRequiredMixin): model = Position - fields = ('name', 'description', 'position_start', 'position_end', - 'reports_to', 'closes', 'application_form') + form_class = UpdateCreatePosition template_name="form_crispy_cbv.html" permission_required="positions.add_position" def get_success_url(self): return reverse("accounts:me") -class ListPositions(generic.ListView): +class UpdatePosition(generic.UpdateView, PermissionRequiredMixin, LoginRequiredMixin): + model = Position + template_name="form_crispy_cbv.html" + permission_required="positions.change_position" + form_class = UpdateCreatePosition + + def get_success_url(self): + return reverse("positions:detail", args=[self.object.id]) + +class ListPositions(generic.ListView, PermissionRequiredMixin, LoginRequiredMixin): model = Position + queryset = Position.objects.filter(closes__gte=datetime.datetime.now()) fields = ('name', 'position_start', 'position_end', 'closes') template_name = 'position_list.html' permission_required = "positions.apply" -class ViewPosition(generic.DetailView): +class ViewPosition(generic.DetailView, PermissionRequiredMixin, LoginRequiredMixin): model = Position template_name = "position_detail.html" permission_required = "positions.apply" diff --git a/site_tmpl/position_detail.html b/site_tmpl/position_detail.html index 035bbf2d..13b4ac0c 100644 --- a/site_tmpl/position_detail.html +++ b/site_tmpl/position_detail.html @@ -8,7 +8,7 @@

    {{ object }}

    Term: {{ object.position_start}} through {{object.position_end}}

    {% permission user has 'positions.change_position' %} - Edit + Edit {% endpermission %} Apply From cad9c571f05250f4a6490e6dc381f6fc5d8c5073 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Fri, 12 Nov 2021 11:57:45 -0500 Subject: [PATCH 05/14] positions: Add the "apply" permission --- positions/migrations/0005_auto_20211112_1115.py | 17 +++++++++++++++++ positions/models.py | 5 +++++ 2 files changed, 22 insertions(+) create mode 100644 positions/migrations/0005_auto_20211112_1115.py diff --git a/positions/migrations/0005_auto_20211112_1115.py b/positions/migrations/0005_auto_20211112_1115.py new file mode 100644 index 00000000..5d6b3d25 --- /dev/null +++ b/positions/migrations/0005_auto_20211112_1115.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.13 on 2021-11-12 16:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('positions', '0004_auto_20211111_1419'), + ] + + operations = [ + migrations.AlterModelOptions( + name='position', + options={'permissions': [('apply', 'Can apply for positions')]}, + ), + ] diff --git a/positions/models.py b/positions/models.py index ad2fed28..f33004a7 100644 --- a/positions/models.py +++ b/positions/models.py @@ -27,6 +27,11 @@ class Position(models.Model): def __str__(self): return f"{self.name}" + class Meta: + permissions = [ + ('apply', 'Can apply for positions') + ] + def is_open(self): return self.closes >= datetime.datetime.now() From 16e03d36b0e554b547df47834ea273317fe7e840 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Fri, 12 Nov 2021 11:58:10 -0500 Subject: [PATCH 06/14] positions: Display a banner on the main page for open positions --- events/views/indices.py | 4 ++++ site_tmpl/admin.html | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/events/views/indices.py b/events/views/indices.py index aafbea4b..4183ee05 100644 --- a/events/views/indices.py +++ b/events/views/indices.py @@ -13,6 +13,7 @@ from events.models import BaseEvent, Workshop, CrewAttendanceRecord from helpers.challenges import is_officer from pages.models import OnboardingScreen, OnboardingRecord +from positions.models import Position # FRONT 3 PAGES @@ -80,6 +81,9 @@ def admin(request, msg=None): datetime_end__gte=(now - datetime.timedelta(hours=3))).distinct() context['selfcrew_events'] = selfcrew_events + open_positions = Position.objects.filter(closes__gte=datetime.datetime.now()).count() + context['open_positions'] = open_positions + return render(request, 'admin.html', context) diff --git a/site_tmpl/admin.html b/site_tmpl/admin.html index 90f3c2c6..57db7dc1 100644 --- a/site_tmpl/admin.html +++ b/site_tmpl/admin.html @@ -106,6 +106,18 @@

    Welcome to the LNL Database

    +
    +
    + {% if open_positions >= 1 %} + {% permission request.user has 'positions.apply' %} +
    +
    There {% if open_positions == 1 %}is {% else %}are {% endif %}{{ open_positions }} + open position{{ open_positions|pluralize }} available! + Check out our list + for more information
    +
    + {% endpermission %} + {% endif %}
    {% endblock %} From 6dad352806b76cf46b819bbdb8fc0fa7dd184f88 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Fri, 12 Nov 2021 11:58:24 -0500 Subject: [PATCH 07/14] positions: Enable Officers to "Add Positions" from the navbar --- site_tmpl/admin.nav.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/site_tmpl/admin.nav.html b/site_tmpl/admin.nav.html index 2b000aef..06144009 100644 --- a/site_tmpl/admin.nav.html +++ b/site_tmpl/admin.nav.html @@ -123,6 +123,10 @@ {% permission request.user has 'positions.apply' or request.user has 'positions.add_position' %}
  • Open Positions
  • + {% permission request.user has 'positions.add_position' %} +
  • Add + Position
  • + {% endpermission %}
  • {% endpermission %} {% permission request.user has 'events.view_attendance_records' %} From 62f6a264d5f73d16dfa6f85b92fb09a5f98c4d46 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Fri, 12 Nov 2021 12:01:05 -0500 Subject: [PATCH 08/14] positions: Remove unneeded ApplicationInstance model --- .../0006_delete_applicationinstance.py | 16 ++++++++++++++++ positions/models.py | 6 ------ 2 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 positions/migrations/0006_delete_applicationinstance.py diff --git a/positions/migrations/0006_delete_applicationinstance.py b/positions/migrations/0006_delete_applicationinstance.py new file mode 100644 index 00000000..aeb95aed --- /dev/null +++ b/positions/migrations/0006_delete_applicationinstance.py @@ -0,0 +1,16 @@ +# Generated by Django 3.1.13 on 2021-11-12 17:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('positions', '0005_auto_20211112_1115'), + ] + + operations = [ + migrations.DeleteModel( + name='ApplicationInstance', + ), + ] diff --git a/positions/models.py b/positions/models.py index f33004a7..23ddc5bf 100644 --- a/positions/models.py +++ b/positions/models.py @@ -35,9 +35,3 @@ class Meta: def is_open(self): return self.closes >= datetime.datetime.now() -class ApplicationInstance(models.Model): - """ - Defines a specific application instance for a user and position. - """ - applicant = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) - position = models.ForeignKey(Position, on_delete=models.CASCADE) From 0bff9cd1ba76b59d3a855be2b0e1b1c22d0bbdef Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Sun, 14 Nov 2021 13:59:41 -0500 Subject: [PATCH 09/14] positions: Add edge case for no report Previously, the template would error trying to reverse to the accounts detail page for None. It now defaults to "Exec Board" as a whole, unless a specific officer is set. --- site_tmpl/position_detail.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/site_tmpl/position_detail.html b/site_tmpl/position_detail.html index 13b4ac0c..83160cae 100644 --- a/site_tmpl/position_detail.html +++ b/site_tmpl/position_detail.html @@ -28,7 +28,10 @@

    Term: {{ object.position_start}} through {{object.position_end}}

    Reports To - {{ object.reports_to }} + + {% if object.reports_to %}{{ object.reports_to }} + {% else %}Exec Board{% endif %} + Applications Close From bab0c11249644dce1373283f50ecce490cf32cb4 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Sun, 14 Nov 2021 14:04:19 -0500 Subject: [PATCH 10/14] positions: Use timezone-aware DateTimes --- events/views/indices.py | 2 +- positions/models.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/events/views/indices.py b/events/views/indices.py index 4183ee05..e07f3176 100644 --- a/events/views/indices.py +++ b/events/views/indices.py @@ -81,7 +81,7 @@ def admin(request, msg=None): datetime_end__gte=(now - datetime.timedelta(hours=3))).distinct() context['selfcrew_events'] = selfcrew_events - open_positions = Position.objects.filter(closes__gte=datetime.datetime.now()).count() + open_positions = Position.objects.filter(closes__gte=timezone.now()).count() context['open_positions'] = open_positions return render(request, 'admin.html', context) diff --git a/positions/models.py b/positions/models.py index 23ddc5bf..f93cad78 100644 --- a/positions/models.py +++ b/positions/models.py @@ -1,5 +1,6 @@ from django.db import models from django.contrib.auth import get_user_model +from django.utils import timezone import datetime @@ -20,7 +21,7 @@ class Position(models.Model): blank=False) reports_to = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, blank=True, null=True) - closes = models.DateField(verbose_name="Applications Close", null=True, + closes = models.DateTimeField(verbose_name="Applications Close", null=True, blank=True) application_form = models.CharField(verbose_name="Link to external application form", null=False, blank=False, max_length=128) @@ -33,5 +34,5 @@ class Meta: ] def is_open(self): - return self.closes >= datetime.datetime.now() + return self.closes >= timezone.now() From 6e15861ea42ad79e74e75fcc7c26c07155552219 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Sun, 14 Nov 2021 14:04:47 -0500 Subject: [PATCH 11/14] positions: Switch to DateTime and URL Fields This will change the `closes` field in the positions app to a DateTime field, and the `application_form` field to be a URLField. --- .../migrations/0007_auto_20211114_1403.py | 23 +++++++++++++++++++ positions/models.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 positions/migrations/0007_auto_20211114_1403.py diff --git a/positions/migrations/0007_auto_20211114_1403.py b/positions/migrations/0007_auto_20211114_1403.py new file mode 100644 index 00000000..9737a8ab --- /dev/null +++ b/positions/migrations/0007_auto_20211114_1403.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.13 on 2021-11-14 19:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('positions', '0006_delete_applicationinstance'), + ] + + operations = [ + migrations.AlterField( + model_name='position', + name='application_form', + field=models.URLField(max_length=128, verbose_name='Link to external application form'), + ), + migrations.AlterField( + model_name='position', + name='closes', + field=models.DateTimeField(blank=True, null=True, verbose_name='Applications Close'), + ), + ] diff --git a/positions/models.py b/positions/models.py index f93cad78..d4c22dc7 100644 --- a/positions/models.py +++ b/positions/models.py @@ -23,7 +23,7 @@ class Position(models.Model): blank=True, null=True) closes = models.DateTimeField(verbose_name="Applications Close", null=True, blank=True) - application_form = models.CharField(verbose_name="Link to external application form", null=False, blank=False, max_length=128) + application_form = models.URLField(verbose_name="Link to external application form", null=False, blank=False, max_length=128) def __str__(self): return f"{self.name}" From 9c82530a14ed3ffb758262d96afc2d6e1409d2aa Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Mon, 13 Dec 2021 12:54:37 -0500 Subject: [PATCH 12/14] positions: Add and fix tests This will test every view for access control, as well as the `is_closed` method on Positions --- positions/tests.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++ positions/views.py | 8 ++--- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/positions/tests.py b/positions/tests.py index 7ce503c2..fcc9182a 100644 --- a/positions/tests.py +++ b/positions/tests.py @@ -1,3 +1,84 @@ from django.test import TestCase +from django.utils import timezone +import datetime + +from data.tests.util import ViewTestCase +from django.contrib.auth.models import Permission +from django.urls.base import reverse + + +from .models import Position # Create your tests here. + +class PositionModelTests(TestCase): + + def setUp(self): + self.position = Position(name="Test", description="Test", + position_start=timezone.now() + timezone.timedelta(days=-2), + position_end=timezone.now(), + closes=timezone.now(), application_form="https://example.com") + + def test_closed(self): + today = timezone.now() + yesterday = today + timezone.timedelta(days=-1) + tomorrow = today + timezone.timedelta(days=1) + + self.position.closes = today + self.position.save() + self.assertFalse(self.position.is_open()) + + self.position.closes = yesterday + self.position.save() + self.assertFalse(self.position.is_open()) + + self.position.closes = tomorrow + self.position.save() + self.assertTrue(self.position.is_open()) + +class PositionViewTests(ViewTestCase): + def setUp(self): + super(PositionViewTests, self).setUp() + + self.position = Position.objects.create(name="Test", description="Test", + position_start=timezone.now() + timezone.timedelta(days=-2), + position_end=timezone.now() + timezone.timedelta(days=10), + closes = timezone.now() + timezone.timedelta(days=1), + application_form="https://example.com") + + def test_listposition(self): + # Should not be able to view positions by default + self.assertOk(self.client.get(reverse("positions:list")), 403) + + perm = Permission.objects.get(codename="apply") + self.user.user_permissions.add(perm) + + self.assertOk(self.client.get(reverse("positions:list"))) + + def test_createposition(self): + self.assertOk(self.client.get(reverse("positions:create")), 403) + + perm = Permission.objects.get(codename="add_position") + self.user.user_permissions.add(perm) + + self.assertOk(self.client.get(reverse("positions:create"))) + + def test_viewposition(self): + self.assertOk(self.client.get(reverse("positions:detail", + args=[self.position.pk])), 403) + + perm = Permission.objects.get(codename="apply") + self.user.user_permissions.add(perm) + + self.assertOk(self.client.get(reverse("positions:detail", + args=[self.position.pk]))) + + def test_updateposition(self): + self.assertOk(self.client.get(reverse("positions:update", + args=[self.position.pk])), 403) + + perm = Permission.objects.get(codename="change_position") + self.user.user_permissions.add(perm) + + self.assertOk(self.client.get(reverse("positions:update", + args=[self.position.pk]))) diff --git a/positions/views.py b/positions/views.py index c0234b2c..b1cdb3c6 100644 --- a/positions/views.py +++ b/positions/views.py @@ -10,7 +10,7 @@ # Create your views here. -class CreatePosition(generic.CreateView, PermissionRequiredMixin, LoginRequiredMixin): +class CreatePosition(PermissionRequiredMixin, LoginRequiredMixin, generic.CreateView): model = Position form_class = UpdateCreatePosition template_name="form_crispy_cbv.html" @@ -19,7 +19,7 @@ class CreatePosition(generic.CreateView, PermissionRequiredMixin, LoginRequiredM def get_success_url(self): return reverse("accounts:me") -class UpdatePosition(generic.UpdateView, PermissionRequiredMixin, LoginRequiredMixin): +class UpdatePosition(PermissionRequiredMixin, LoginRequiredMixin, generic.UpdateView): model = Position template_name="form_crispy_cbv.html" permission_required="positions.change_position" @@ -28,14 +28,14 @@ class UpdatePosition(generic.UpdateView, PermissionRequiredMixin, LoginRequiredM def get_success_url(self): return reverse("positions:detail", args=[self.object.id]) -class ListPositions(generic.ListView, PermissionRequiredMixin, LoginRequiredMixin): +class ListPositions(PermissionRequiredMixin, LoginRequiredMixin, generic.ListView): model = Position queryset = Position.objects.filter(closes__gte=datetime.datetime.now()) fields = ('name', 'position_start', 'position_end', 'closes') template_name = 'position_list.html' permission_required = "positions.apply" -class ViewPosition(generic.DetailView, PermissionRequiredMixin, LoginRequiredMixin): +class ViewPosition(PermissionRequiredMixin, LoginRequiredMixin, generic.DetailView): model = Position template_name = "position_detail.html" permission_required = "positions.apply" From b926a9f51774b596b722eb40a9d8c3ff17b103d4 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Mon, 13 Dec 2021 13:00:33 -0500 Subject: [PATCH 13/14] positions: Don't require an external application form Some positions do not require applications (e.g Executive board positions). See https://github.com/WPI-LNL/lnldb/pull/577#discussion_r748475091 Signed-off-by: Cara Salter --- .../migrations/0008_auto_20211213_1256.py | 18 ++++++++++++++++++ positions/models.py | 2 +- site_tmpl/position_detail.html | 2 ++ site_tmpl/position_list.html | 2 +- 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 positions/migrations/0008_auto_20211213_1256.py diff --git a/positions/migrations/0008_auto_20211213_1256.py b/positions/migrations/0008_auto_20211213_1256.py new file mode 100644 index 00000000..07301e90 --- /dev/null +++ b/positions/migrations/0008_auto_20211213_1256.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.13 on 2021-12-13 17:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('positions', '0007_auto_20211114_1403'), + ] + + operations = [ + migrations.AlterField( + model_name='position', + name='application_form', + field=models.URLField(blank=True, max_length=128, null=True, verbose_name='Link to external application form'), + ), + ] diff --git a/positions/models.py b/positions/models.py index d4c22dc7..81ef3b45 100644 --- a/positions/models.py +++ b/positions/models.py @@ -23,7 +23,7 @@ class Position(models.Model): blank=True, null=True) closes = models.DateTimeField(verbose_name="Applications Close", null=True, blank=True) - application_form = models.URLField(verbose_name="Link to external application form", null=False, blank=False, max_length=128) + application_form = models.URLField(verbose_name="Link to external application form", null=True, blank=True, max_length=128) def __str__(self): return f"{self.name}" diff --git a/site_tmpl/position_detail.html b/site_tmpl/position_detail.html index 83160cae..981eee41 100644 --- a/site_tmpl/position_detail.html +++ b/site_tmpl/position_detail.html @@ -10,7 +10,9 @@

    Term: {{ object.position_start}} through {{object.position_end}}

    {% permission user has 'positions.change_position' %} Edit {% endpermission %} + {% if object.application_form %} Apply + {% endif %} diff --git a/site_tmpl/position_list.html b/site_tmpl/position_list.html index e7344305..18a03385 100644 --- a/site_tmpl/position_list.html +++ b/site_tmpl/position_list.html @@ -3,7 +3,7 @@ {% block content %}

    Open positions

    -

    These are positions that the executive board is seeking applications for. +

    These are positions that the executive board is looking to fill. Click on a position name to view more details.

    From d1906c8cfb33c8c1e786edbf210849b2618d4055 Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Mon, 13 Dec 2021 13:14:27 -0500 Subject: [PATCH 14/14] docs: Document positions app Signed-off-by: Cara Salter --- docs/help/positions/apply.rst | 25 +++++++++++++++++++++++++ docs/help/positions/create.rst | 0 docs/help/user-guides.rst | 8 ++++++++ 3 files changed, 33 insertions(+) create mode 100644 docs/help/positions/apply.rst create mode 100644 docs/help/positions/create.rst diff --git a/docs/help/positions/apply.rst b/docs/help/positions/apply.rst new file mode 100644 index 00000000..a4c50632 --- /dev/null +++ b/docs/help/positions/apply.rst @@ -0,0 +1,25 @@ +==================== +Apply for a position +==================== + +Active members can now see a list of open leadership positions created by the +Executive Board. + +Viewing Positions +----------------- +To view a list of open positions, go to the "Members" dropdown and find the +"Open Positions" link. Clicking that link will bring you to a list of positions +that are currently looking for interested members. + +.. hint: + You can click the name of a position to view more information + +Applying +-------- +Depending on how the Executive board has created the position, you can do one of +two things to express your interest. If they have created a form for interested +members, there will be a green "Apply" button that will take you to the form. If +they haven't created a form, look at the position description for more +information. + +`Last modified: 13 December 2021` diff --git a/docs/help/positions/create.rst b/docs/help/positions/create.rst new file mode 100644 index 00000000..e69de29b diff --git a/docs/help/user-guides.rst b/docs/help/user-guides.rst index c16d0cd5..707d9654 100644 --- a/docs/help/user-guides.rst +++ b/docs/help/user-guides.rst @@ -79,3 +79,11 @@ Training training/request-pit training/training-records + +Positions +^^^^^^^^^ + +.. toctree:: + :maxdepth: 1 + positions/apply + positions/create