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 %} -
+ {% csrf_token %}
{% 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