From abda943e69f19f4a5d6453eb9b07ad8b7710d27d Mon Sep 17 00:00:00 2001
From: Stefan Dworschak
Date: Mon, 19 Dec 2022 16:56:33 +0000
Subject: [PATCH 1/2] Changing logic to create private channels instead of IM
Groups
---
accounts/admin.py | 5 +-
accounts/migrations/0018_slacksitesettings.py | 27 +++
accounts/models.py | 30 +++
.../migrations/0003_auto_20221219_1421.py | 21 +++
.../migrations/0047_auto_20221219_1421.py | 18 ++
.../migrations/0048_auto_20221219_1655.py | 18 ++
hackathon/models.py | 2 +-
.../templates/hackathon/hackathon_view.html | 3 +
.../hackathon/includes/enrollpart.html | 2 +-
hackathon/views.py | 4 +-
home/models.py | 2 +-
home/tests.py | 176 ++++++++++++++++++
main/models.py | 19 ++
main/settings.py | 1 +
showcase/models.py | 19 +-
...html => create_slack_private_channel.html} | 10 +-
teams/templates/team.html | 2 +-
teams/urls.py | 4 +-
teams/views.py | 108 ++++++++---
19 files changed, 408 insertions(+), 63 deletions(-)
create mode 100644 accounts/migrations/0018_slacksitesettings.py
create mode 100644 competencies/migrations/0003_auto_20221219_1421.py
create mode 100644 hackathon/migrations/0047_auto_20221219_1421.py
create mode 100644 hackathon/migrations/0048_auto_20221219_1655.py
create mode 100644 home/tests.py
create mode 100644 main/models.py
rename teams/templates/includes/{create_slack_mpim.html => create_slack_private_channel.html} (69%)
diff --git a/accounts/admin.py b/accounts/admin.py
index e081ffd5..70cedd0b 100644
--- a/accounts/admin.py
+++ b/accounts/admin.py
@@ -4,6 +4,7 @@
from django.contrib.auth.decorators import login_required
from .models import CustomUser, Organisation
+from accounts.models import SlackSiteSettings
class CustomUserAdmin(BaseUserAdmin):
@@ -38,7 +39,7 @@ class CustomUserAdmin(BaseUserAdmin):
form = UserChangeForm
add_form = UserCreationForm
- list_display = ('email', 'full_name', 'is_superuser', 'user_type',
+ list_display = ('email', 'username', 'full_name', 'is_superuser', 'user_type',
'is_external')
list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups',
'is_external')
@@ -52,3 +53,5 @@ class CustomUserAdmin(BaseUserAdmin):
admin.site.login = login_required(admin.site.login)
admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(Organisation)
+
+admin.site.register(SlackSiteSettings)
\ No newline at end of file
diff --git a/accounts/migrations/0018_slacksitesettings.py b/accounts/migrations/0018_slacksitesettings.py
new file mode 100644
index 00000000..af9d6806
--- /dev/null
+++ b/accounts/migrations/0018_slacksitesettings.py
@@ -0,0 +1,27 @@
+# Generated by Django 3.1.13 on 2022-12-19 14:45
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0017_customuser_timezone'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='SlackSiteSettings',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('enable_welcome_emails', models.BooleanField(default=True)),
+ ('communication_channel_type', models.CharField(choices=[('slack_private_channel', 'Private Slack Channel'), ('other', 'Other')], default='slack_private_channel', max_length=50)),
+ ('slack_admins', models.ManyToManyField(related_name='slacksitesettings', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'Slack Site Settings',
+ 'verbose_name_plural': 'Slack Site Settings',
+ },
+ ),
+ ]
diff --git a/accounts/models.py b/accounts/models.py
index 5722c61f..4735770b 100644
--- a/accounts/models.py
+++ b/accounts/models.py
@@ -6,8 +6,14 @@
from django.contrib.auth.models import AbstractUser
from .lists import LMS_MODULES_CHOICES, TIMEZONE_CHOICES
+from main.models import SingletonModel
from teams.lists import LMS_LEVELS
+COMMUNICATION_CHANNEL_TYPES = [
+ ('slack_private_channel', 'Private Slack Channel'),
+ ('other', 'Other'),
+]
+
class UserType(Enum):
SUPERUSER = 0
@@ -144,6 +150,13 @@ def participant_label(self):
return 'Hackathon Enthusiast'
else:
return 'Hackathon Veteran'
+
+ def is_participant(self, hackathon):
+ if not hackathon:
+ return False
+
+ return self in hackathon.participants.all()
+
@property
def user_type(self):
@@ -180,3 +193,20 @@ def user_type(self):
else:
# A non-specified group
return None
+
+
+class SlackSiteSettings(SingletonModel):
+ """ Model to set how the showcase should be constructed"""
+ slack_admins = models.ManyToManyField(CustomUser,
+ related_name="slacksitesettings")
+ enable_welcome_emails = models.BooleanField(default=True)
+ communication_channel_type = models.CharField(
+ max_length=50, choices=COMMUNICATION_CHANNEL_TYPES,
+ default='slack_private_channel')
+
+ def __str__(self):
+ return "Slack Settings"
+
+ class Meta:
+ verbose_name = 'Slack Site Settings'
+ verbose_name_plural = 'Slack Site Settings'
diff --git a/competencies/migrations/0003_auto_20221219_1421.py b/competencies/migrations/0003_auto_20221219_1421.py
new file mode 100644
index 00000000..147c97cb
--- /dev/null
+++ b/competencies/migrations/0003_auto_20221219_1421.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.1.13 on 2022-12-19 14:21
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('competencies', '0002_auto_20220808_1029'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='competencyassessment',
+ options={'verbose_name': 'Competency Self Assessment', 'verbose_name_plural': 'Competency Self Assessments'},
+ ),
+ migrations.AlterModelOptions(
+ name='competencyassessmentrating',
+ options={'verbose_name': 'Competency Self Assessment Rating', 'verbose_name_plural': 'Competency Self Assessment Ratings'},
+ ),
+ ]
diff --git a/hackathon/migrations/0047_auto_20221219_1421.py b/hackathon/migrations/0047_auto_20221219_1421.py
new file mode 100644
index 00000000..ea171859
--- /dev/null
+++ b/hackathon/migrations/0047_auto_20221219_1421.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.13 on 2022-12-19 14:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('hackathon', '0046_auto_20220113_1350'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='hackathon',
+ name='is_public',
+ field=models.BooleanField(default=True),
+ ),
+ ]
diff --git a/hackathon/migrations/0048_auto_20221219_1655.py b/hackathon/migrations/0048_auto_20221219_1655.py
new file mode 100644
index 00000000..51aa259c
--- /dev/null
+++ b/hackathon/migrations/0048_auto_20221219_1655.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.13 on 2022-12-19 16:55
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('hackathon', '0047_auto_20221219_1421'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='hackteam',
+ name='communication_channel',
+ field=models.CharField(blank=True, default='', help_text='Usually a link to the Private Slack Channel, but can be a link to something else.', max_length=255),
+ ),
+ ]
diff --git a/hackathon/models.py b/hackathon/models.py
index d4f2c12c..6e190dd7 100644
--- a/hackathon/models.py
+++ b/hackathon/models.py
@@ -188,7 +188,7 @@ class HackTeam(models.Model):
on_delete=models.SET_NULL)
communication_channel = models.CharField(
default="", max_length=255, blank=True,
- help_text=("Usually a link to the Slack group IM, but can be a link "
+ help_text=("Usually a link to the Private Slack Channel, but can be a link "
"to something else."))
def __str__(self):
diff --git a/hackathon/templates/hackathon/hackathon_view.html b/hackathon/templates/hackathon/hackathon_view.html
index 1804cba5..c7f8e76e 100644
--- a/hackathon/templates/hackathon/hackathon_view.html
+++ b/hackathon/templates/hackathon/hackathon_view.html
@@ -99,6 +99,9 @@
Status: {{ hackathon.status }}
Organiser: {{ hackathon.organiser }}
+ Participants: {{ hackathon.participants.all|length }} / Teams: {{ hackathon.teams.all|length }}
+
+
Max Participants: {% if hackathon.max_participants %}{{ hackathon.max_participants }}{% else %}Unlimited{% endif %}
{% if hackathon.max_participants_reached %}(Max Reached){% endif %}
diff --git a/hackathon/templates/hackathon/includes/enrollpart.html b/hackathon/templates/hackathon/includes/enrollpart.html
index ca578ef4..0f4e63d6 100644
--- a/hackathon/templates/hackathon/includes/enrollpart.html
+++ b/hackathon/templates/hackathon/includes/enrollpart.html
@@ -46,7 +46,7 @@
{{participant_team}}
- {% include 'includes/create_slack_mpim.html' with team=participant_team button_class='btn btn-sm btn-ci' %}
+ {% include 'includes/create_slack_private_channel.html' with team=participant_team button_class='btn btn-sm btn-ci' %}
{% else %}
You have not been assigned a team yet.
diff --git a/hackathon/views.py b/hackathon/views.py
index e4c60333..ca74babc 100644
--- a/hackathon/views.py
+++ b/hackathon/views.py
@@ -360,13 +360,13 @@ def view_hackathon(request, hackathon_id):
paginator = Paginator(teams, 3)
page = request.GET.get('page')
paged_teams = paginator.get_page(page)
- create_group_im = (settings.SLACK_ENABLED and settings.SLACK_BOT_TOKEN)
+ create_private_channel = (settings.SLACK_ENABLED and settings.SLACK_BOT_TOKEN)
context = {
'hackathon': hackathon,
'teams': paged_teams,
'change_status_form': ChangeHackathonStatusForm(instance=hackathon),
- 'create_group_im': create_group_im,
+ 'create_private_channel': create_private_channel,
}
return render(request, "hackathon/hackathon_view.html", context)
diff --git a/home/models.py b/home/models.py
index b008447e..b956ed20 100644
--- a/home/models.py
+++ b/home/models.py
@@ -1,7 +1,7 @@
from django.db import models
from accounts.models import CustomUser as User
-from showcase.models import SingletonModel
+from main.models import SingletonModel
class Review(models.Model):
diff --git a/home/tests.py b/home/tests.py
new file mode 100644
index 00000000..eb18b641
--- /dev/null
+++ b/home/tests.py
@@ -0,0 +1,176 @@
+
+from django.shortcuts import reverse
+from django.test import TestCase
+from accounts.models import CustomUser, Organisation
+from django.utils import timezone
+
+from hackathon.models import Hackathon
+
+
+class TestHackathonViews(TestCase):
+ def setUp(self):
+ self.organisation = Organisation.objects.create(display_name="CI")
+ self.partner_org = Organisation.objects.create(display_name="Partner")
+ self.user = CustomUser.objects.create(username="testuser", organisation=self.organisation)
+ self.partner_user = CustomUser.objects.create(username="partnertestuser", organisation=self.partner_org)
+ self.staff_user = CustomUser.objects.create(username="staffuser")
+ self.staff_user.is_staff = True
+ self.staff_user.save()
+ self.super_user = CustomUser.objects.create(username="super_user")
+ self.super_user.is_staff = True
+ self.super_user.is_superuser = True
+ self.super_user.save()
+ self.hackathon = Hackathon.objects.create(
+ created_by=self.user,
+ status='published',
+ display_name="hacktest",
+ description="lorem ipsum",
+ start_date=f'{timezone.now()}',
+ end_date=f'{timezone.now()}',
+ organisation=self.organisation,
+ is_public=True)
+
+ def test_list_hackathons_for_non_authenticated_user(self):
+ hackathon = Hackathon.objects.create(
+ created_by=self.user,
+ status='finished',
+ display_name="hacktest2",
+ description="lorem ipsum",
+ start_date=f'{timezone.now()}',
+ end_date=f'{timezone.now()}',
+ organisation=self.partner_org,
+ is_public=False)
+
+ hackathon2 = Hackathon.objects.create(
+ created_by=self.user,
+ status='published',
+ display_name="hacktest3",
+ description="lorem ipsum",
+ start_date=f'{timezone.now()}',
+ end_date=f'{timezone.now()}',
+ organisation=self.partner_org,
+ is_public=False)
+
+ hackathon3 = Hackathon.objects.create(
+ created_by=self.user,
+ status='finished',
+ display_name="hacktest2",
+ description="lorem ipsum",
+ start_date=f'{timezone.now()}',
+ end_date=f'{timezone.now()}',
+ organisation=self.organisation,
+ is_public=False)
+
+ # if this is more than 5, the response results will have to be paginated
+ # because they are capped at 5
+ num_hackathons = Hackathon.objects.count()
+ self.assertTrue(num_hackathons <= 5)
+
+ response = self.client.get(reverse('home'))
+ recent_hackathons = [hackathon.id for hackathon in response.context['recent_hackathons']]
+ upcoming_hackathons = [hackathon.id for hackathon in response.context['upcoming_hackathons']]
+ self.assertEquals(len(recent_hackathons), 0)
+ self.assertEquals(len(upcoming_hackathons), 1)
+ self.assertTrue(hackathon.id not in recent_hackathons)
+ self.assertTrue(hackathon2.id not in upcoming_hackathons)
+
+ hackathon3.is_public = True
+ hackathon3.save()
+
+ response = self.client.get(reverse('home'))
+ recent_hackathons = [hackathon.id for hackathon in response.context['recent_hackathons']]
+ upcoming_hackathons = [hackathon.id for hackathon in response.context['upcoming_hackathons']]
+ self.assertEquals(len(recent_hackathons), 1)
+ self.assertEquals(len(upcoming_hackathons), 1)
+
+ hackathon2.is_public = True
+ hackathon2.save()
+
+ response = self.client.get(reverse('home'))
+ recent_hackathons = [hackathon.id for hackathon in response.context['recent_hackathons']]
+ upcoming_hackathons = [hackathon.id for hackathon in response.context['upcoming_hackathons']]
+ self.assertEquals(len(upcoming_hackathons), 2)
+ self.assertEquals(len(recent_hackathons), 1)
+
+ self.user.organisation = self.partner_org
+ self.user.save()
+ self.client.force_login(self.user)
+
+ response = self.client.get(reverse('home'))
+ recent_hackathons = [hackathon.id for hackathon in response.context['recent_hackathons']]
+ upcoming_hackathons = [hackathon.id for hackathon in response.context['upcoming_hackathons']]
+ self.assertEquals(len(upcoming_hackathons), 2)
+ self.assertEquals(len(recent_hackathons), 2)
+
+ def test_list_partner_hackathons_on_home(self):
+ hackathon = Hackathon.objects.create(
+ created_by=self.user,
+ status='finished',
+ display_name="hacktest2",
+ description="lorem ipsum",
+ start_date=f'{timezone.now()}',
+ end_date=f'{timezone.now()}',
+ organisation=self.partner_org,
+ is_public=False)
+
+ hackathon2 = Hackathon.objects.create(
+ created_by=self.user,
+ status='published',
+ display_name="hacktest3",
+ description="lorem ipsum",
+ start_date=f'{timezone.now()}',
+ end_date=f'{timezone.now()}',
+ organisation=self.partner_org,
+ is_public=False)
+
+ # if this is more than 5, the response results will have to be paginated
+ # because they are capped at 5
+ num_hackathons = Hackathon.objects.count()
+ self.assertTrue(num_hackathons <= 5)
+
+ self.client.force_login(self.user)
+ response = self.client.get(reverse('home'))
+ recent_hackathons = [hackathon.id for hackathon in response.context['recent_hackathons']]
+ upcoming_hackathons = [hackathon.id for hackathon in response.context['upcoming_hackathons']]
+ self.assertTrue(hackathon.id not in recent_hackathons)
+ self.assertTrue(hackathon2.id not in upcoming_hackathons)
+
+ self.client.force_login(self.staff_user)
+ response = self.client.get(reverse('home'))
+ recent_hackathons = [hackathon.id for hackathon in response.context['recent_hackathons']]
+ upcoming_hackathons = [hackathon.id for hackathon in response.context['upcoming_hackathons']]
+ self.assertTrue(hackathon.id in recent_hackathons)
+ self.assertTrue(hackathon2.id in upcoming_hackathons)
+
+ self.client.force_login(self.super_user)
+ response = self.client.get(reverse('home'))
+ recent_hackathons = [hackathon.id for hackathon in response.context['recent_hackathons']]
+ upcoming_hackathons = [hackathon.id for hackathon in response.context['upcoming_hackathons']]
+ self.assertTrue(hackathon.id in recent_hackathons)
+ self.assertTrue(hackathon2.id in upcoming_hackathons)
+
+ hackathon.is_public = True
+ hackathon.save()
+ hackathon2.is_public = True
+ hackathon2.save()
+
+ self.client.force_login(self.user)
+ response = self.client.get(reverse('home'))
+ recent_hackathons = [hackathon.id for hackathon in response.context['recent_hackathons']]
+ upcoming_hackathons = [hackathon.id for hackathon in response.context['upcoming_hackathons']]
+ self.assertTrue(hackathon.id in recent_hackathons)
+ self.assertTrue(hackathon2.id in upcoming_hackathons)
+
+ hackathon.is_public = False
+ hackathon.save()
+ hackathon2.is_public = False
+ hackathon2.save()
+ self.user.organisation = self.partner_org
+ self.user.save()
+
+ self.client.force_login(self.user)
+ response = self.client.get(reverse('home'))
+ recent_hackathons = [hackathon.id for hackathon in response.context['recent_hackathons']]
+ upcoming_hackathons = [hackathon.id for hackathon in response.context['upcoming_hackathons']]
+ self.assertTrue(hackathon.id in recent_hackathons)
+ self.assertTrue(hackathon2.id in upcoming_hackathons)
diff --git a/main/models.py b/main/models.py
new file mode 100644
index 00000000..5d3967c2
--- /dev/null
+++ b/main/models.py
@@ -0,0 +1,19 @@
+from django.db import models
+
+
+class SingletonModel(models.Model):
+ """ Singleton model for Showcases """
+ class Meta:
+ abstract = True
+
+ def save(self, *args, **kwargs):
+ self.pk = 1
+ super(SingletonModel, self).save(*args, **kwargs)
+
+ def delete(self, *args, **kwargs):
+ pass
+
+ @classmethod
+ def load(cls):
+ obj, created = cls.objects.get_or_create(pk=1)
+ return obj
diff --git a/main/settings.py b/main/settings.py
index 5268a5c2..197d84c1 100644
--- a/main/settings.py
+++ b/main/settings.py
@@ -183,6 +183,7 @@
if SLACK_ENABLED:
SLACK_WORKSPACE = os.environ.get('SLACK_WORKSPACE')
+ SLACK_TEAM_ID = os.environ.get('SLACK_TEAM_ID')
SLACK_BOT_TOKEN = os.environ.get('SLACK_BOT_TOKEN')
INSTALLED_APPS += ['custom_slack_provider']
SOCIALACCOUNT_PROVIDERS = {
diff --git a/showcase/models.py b/showcase/models.py
index 23908929..cba3f804 100644
--- a/showcase/models.py
+++ b/showcase/models.py
@@ -6,6 +6,7 @@
from .lists import ORDER_BY_CATEGORY_CHOICES
from accounts.models import CustomUser as User
from hackathon.models import HackProject, Hackathon
+from main.models import SingletonModel
class Showcase(models.Model):
@@ -70,24 +71,6 @@ def image_url(self):
return f'{self.url}image/{self.hash.hex}/'
-class SingletonModel(models.Model):
- """ Singleton model for Showcases """
- class Meta:
- abstract = True
-
- def save(self, *args, **kwargs):
- self.pk = 1
- super(SingletonModel, self).save(*args, **kwargs)
-
- def delete(self, *args, **kwargs):
- pass
-
- @classmethod
- def load(cls):
- obj, created = cls.objects.get_or_create(pk=1)
- return obj
-
-
class ShowcaseSiteSettings(SingletonModel):
""" Model to set how the showcase should be constructed"""
hackathons = models.ManyToManyField(Hackathon,
diff --git a/teams/templates/includes/create_slack_mpim.html b/teams/templates/includes/create_slack_private_channel.html
similarity index 69%
rename from teams/templates/includes/create_slack_mpim.html
rename to teams/templates/includes/create_slack_private_channel.html
index 4a0402e3..5edec6fa 100644
--- a/teams/templates/includes/create_slack_mpim.html
+++ b/teams/templates/includes/create_slack_private_channel.html
@@ -1,16 +1,16 @@
-{% if create_group_im %}
- {% if request.user in team.participants.all or request.user == team.mentor %}
+{% if create_private_channel %}
+ {% if request.user in team.participants.all or request.user == team.mentor or non_participant_superuser %}
{% if team.communication_channel %}
- Go To Slack Group
+ Go To Private Slack Channel
{% else %}
-
{% endif %}
diff --git a/teams/templates/team.html b/teams/templates/team.html
index 1540c46c..4221da95 100644
--- a/teams/templates/team.html
+++ b/teams/templates/team.html
@@ -60,7 +60,7 @@ About the team
Team Actions