From 6bfbe73006001ed6fdd87041723914abc928bdf2 Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Tue, 18 Jan 2022 14:40:37 +0000 Subject: [PATCH 01/22] Adding celery with Redis implementation --- docker-compose.yml | 18 +++++++++++++++--- hackathon/tests/task_tests.py | 5 +++++ main/settings.py | 10 ++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 hackathon/tests/task_tests.py diff --git a/docker-compose.yml b/docker-compose.yml index 1f83bf32..ba8bdb1c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ services: - hackathon-app: + hackathon-app: &hackathon-app-main image: hackathon-app volumes: - ./staticfiles/:/hackathon-app/staticfiles/ @@ -58,7 +58,19 @@ services: smtp: image: mailhog/mailhog:v1.0.1 ports: - - "8026:8025" + - "8025:8025" redis: - image: redis + image: redis:6.2.4 + ports: + - "6379:6379" + + worker: + <<: *hackathon-app-main + entrypoint: ["celery", "-A", "main", "worker", "-l", "INFO"] + ports: [] + + # environment: + # - ENV_FILE=/hackathon-app/.env + # volumes: + # - ./.env:/hackathon-app/.env diff --git a/hackathon/tests/task_tests.py b/hackathon/tests/task_tests.py new file mode 100644 index 00000000..2a342d40 --- /dev/null +++ b/hackathon/tests/task_tests.py @@ -0,0 +1,5 @@ +from django.test import TestCase + +class TaskTests(TestCase): + def test_simple_task(self): + pass \ No newline at end of file diff --git a/main/settings.py b/main/settings.py index 4ff6399f..c292e0ab 100644 --- a/main/settings.py +++ b/main/settings.py @@ -35,6 +35,8 @@ "allauth", "allauth.account", "allauth.socialaccount", + "django_celery_beat", + "django_celery_results", "crispy_forms", "django_celery_results", @@ -222,3 +224,11 @@ dsn=os.environ.get('SENTRY_DSN'), integrations=[DjangoIntegration()] ) + + +CELERY_IMPORTS = ("hackathon.tasks", ) +CELERY_BROKER_URL = os.environ.get('CELERY_BROKER', 'redis://redis:6379') +CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://redis:6379') # noqa: E501 +CELERY_ACCEPT_CONTENT = os.environ.get('CELERY_ACCEPT_CONTENT', 'application/json').split(',') # noqa: E501 +CELERY_TASK_SERIALIZER = os.environ.get('CELERY_TASK_SERIALIZER', 'json') +CELERY_RESULT_SERIALIZER = os.environ.get('CELERY_RESULT_SERIALIZER', 'json') From 56b1696e8a2b4d33a4057be57ba9ffa8cb84c518 Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Tue, 18 Jan 2022 14:40:55 +0000 Subject: [PATCH 02/22] Updating Slack authentication --- custom_slack_provider/views.py | 38 ++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/custom_slack_provider/views.py b/custom_slack_provider/views.py index 391543ad..8543f51a 100644 --- a/custom_slack_provider/views.py +++ b/custom_slack_provider/views.py @@ -1,5 +1,6 @@ import logging import requests +from requests.auth import AuthBase from accounts.models import CustomUser from django.core.exceptions import PermissionDenied @@ -29,6 +30,18 @@ logger = logging.getLogger(__name__) +AUTHORIZATION_HEADER = {'Authorization': "Bearer %s"} + + +class AuthBearer(AuthBase): + """ Custom requests authentication class for header """ + def __init__(self, token): + self.token = token + + def __call__(self, request): + request.headers["authorization"] = "Bearer " + self.token + return request + class SlackOAuth2Adapter(OAuth2Adapter): provider_id = SlackProvider.id @@ -43,28 +56,41 @@ def complete_login(self, request, app, token, **kwargs): return self.get_provider().sociallogin_from_response(request, extra_data) - def get_data(self, token): - # Verify the user first + def get_identity(self, token): resp = requests.get( self.identity_url, - params={'token': token} + auth=AuthBearer(token), ) resp = resp.json() if not resp.get('ok'): raise OAuth2Error(f'UserInfo Exception: {resp.get("error")}') - userid = resp.get('user', {}).get('id') + return resp + + def get_user_info(self, token, userid): user_info = requests.get( self.user_detail_url, - params={'token': settings.SLACK_BOT_TOKEN, 'user': userid} + params={'user': userid}, + auth=AuthBearer(token), ) user_info = user_info.json() if not user_info.get('ok'): raise OAuth2Error(f'UserInfo Exception: {user_info.get("error")}') - user_info = user_info.get('user', {}) + return user_info.get('user', {}) + + def get_data(self, token): + # Verify the user first + resp = self.get_identity(token) + + userid = resp.get('user', {}).get('id') + user_info = requests.get( + self.user_detail_url, + params={'token': settings.SLACK_BOT_TOKEN, 'user': userid} + ) + user_info = self.get_user_info(token, userid) display_name = user_info.get('profile', {}).get('display_name_normalized') teamid = resp.get('team').get('id') From 763d3884a8643ea574c35edf0b5474bd451aa6f5 Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Tue, 18 Jan 2022 14:42:10 +0000 Subject: [PATCH 03/22] sAdding field to capture channel prefix for Slack implementation --- hackathon/forms.py | 6 ++++++ .../0047_hackathon_channel_prefix.py | 18 ++++++++++++++++++ hackathon/models.py | 3 +++ .../templates/hackathon/create-event.html | 5 +++++ hackathon/views.py | 4 +++- 5 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 hackathon/migrations/0047_hackathon_channel_prefix.py diff --git a/hackathon/forms.py b/hackathon/forms.py index c20c61a3..13f8a172 100644 --- a/hackathon/forms.py +++ b/hackathon/forms.py @@ -105,6 +105,12 @@ class HackathonForm(forms.ModelForm): widget=forms.TextInput({'type': 'number', 'placeholder': 'Leave empty for no max'}) ) + channel_prefix = forms.CharField( + label="Channel Prefix", + required=True, + widget=forms.TextInput(), + ) + class Meta: model = Hackathon fields = ['display_name', 'description', 'theme', 'start_date', diff --git a/hackathon/migrations/0047_hackathon_channel_prefix.py b/hackathon/migrations/0047_hackathon_channel_prefix.py new file mode 100644 index 00000000..d18a5963 --- /dev/null +++ b/hackathon/migrations/0047_hackathon_channel_prefix.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.13 on 2022-01-17 14:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hackathon', '0046_auto_20220113_1350'), + ] + + operations = [ + migrations.AddField( + model_name='hackathon', + name='channel_prefix', + field=models.CharField(blank=True, help_text="Only use lowercase and dash ('-') for spaces", max_length=255, null=True), + ), + ] diff --git a/hackathon/models.py b/hackathon/models.py index a7a5236d..2d312764 100644 --- a/hackathon/models.py +++ b/hackathon/models.py @@ -87,6 +87,9 @@ class Hackathon(models.Model): blank=True, help_text=("Link to the Google Form for registrations.") ) + channel_prefix = models.CharField( + max_length=255, null=True, blank=True, + help_text=("Only use lowercase and dash ('-') for spaces")) def __str__(self): return self.display_name diff --git a/hackathon/templates/hackathon/create-event.html b/hackathon/templates/hackathon/create-event.html index f7196daf..c9999ea0 100644 --- a/hackathon/templates/hackathon/create-event.html +++ b/hackathon/templates/hackathon/create-event.html @@ -40,6 +40,11 @@

Create Hackathon

{{ form.organisation|as_crispy_field }}
+ {% if slack_enabled %} +
+ {{ form.channel_prefix|as_crispy_field }} +
+ {% endif %}
{{ form.team_size|as_crispy_field }}
diff --git a/hackathon/views.py b/hackathon/views.py index fdea951d..8c425754 100644 --- a/hackathon/views.py +++ b/hackathon/views.py @@ -254,7 +254,8 @@ def create_hackathon(request): 'score_categories': HackProjectScoreCategory.objects.filter( is_active=True)[:5]}) - return render(request, template, {"form": form}) + return render(request, template, { + "form": form, "slack_enabled": settings.SLACK_ENABLED}) else: form = HackathonForm(request.POST) @@ -315,6 +316,7 @@ def update_hackathon(request, hackathon_id): context = { "form": form, "hackathon_id": hackathon_id, + "slack_enabled": settings.SLACK_ENABLED, } return render(request, "hackathon/create-event.html", context) From 7df6c47fbafbe25ab26174903608d6a754554284 Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Wed, 19 Jan 2022 14:06:51 +0000 Subject: [PATCH 04/22] Updating the Slack authentication and requests --- custom_slack_provider/adapter.py | 3 +- custom_slack_provider/slack.py | 64 ++++++++++++++++++++++++++++++++ custom_slack_provider/views.py | 53 ++++---------------------- 3 files changed, 74 insertions(+), 46 deletions(-) create mode 100644 custom_slack_provider/slack.py diff --git a/custom_slack_provider/adapter.py b/custom_slack_provider/adapter.py index 4ed52d3b..8de4cc61 100644 --- a/custom_slack_provider/adapter.py +++ b/custom_slack_provider/adapter.py @@ -40,7 +40,8 @@ def populate_user(self, """ username = data.get('username') full_name = data.get('full_name') - slack_display_name = data.get('slack_display_name') + slack_display_name = (data.get('slack_display_name') + or full_name or data.get('email').split('@')[0]) first_name = data.get('first_name') last_name = data.get('last_name') email = data.get('email') diff --git a/custom_slack_provider/slack.py b/custom_slack_provider/slack.py new file mode 100644 index 00000000..2077812d --- /dev/null +++ b/custom_slack_provider/slack.py @@ -0,0 +1,64 @@ +import requests +from requests.auth import AuthBase + +from django.conf import settings + + +class AuthBearer(AuthBase): + """ Custom requests authentication class for header """ + def __init__(self, token): + self.token = token + + def __call__(self, request): + request.headers["authorization"] = "Bearer " + self.token + return request + + +class SlackException(Exception): + def __init__(self, msg): + raise Exception(msg) + + +class CustomSlackClient(): + identity_url = 'https://slack.com/api/users.identity' + user_detail_url = 'https://slack.com/api/users.info' + + def __init__(self, token): + self.token = token + + def _make_slack_get_request(self, url, params=None): + resp = requests.get(url, auth=AuthBearer(self.token), params=params) + resp = resp.json() + + if not resp.get('ok'): + raise SlackException(resp.get("error")) + + return resp + + def _make_slack_post_request(self, url, data): + resp = requests.post(url, auth=AuthBearer(self.token), data=data) + resp = resp.json() + + if not resp.get('ok'): + raise SlackException(resp.get("error")) + + return resp + + def get_identity(self): + return self._make_slack_get_request(self.identity_url) + + def get_user_info(self, userid): + params = {"user": userid} + response = self._make_slack_get_request(self.user_detail_url, + params=params) + return response.get('user', {}) + + def create_slack_channel(self, channel_name, is_private=True): + data = { + "name": channel_name, + "is_private": is_private, + "team_id": settings.SLACK_TEAM_ID, + } + new_channel = self._make_slack_post_request(self.user_detail_url, + data=data) + return new_channel.get('channel', {}) diff --git a/custom_slack_provider/views.py b/custom_slack_provider/views.py index 8543f51a..3251ff5e 100644 --- a/custom_slack_provider/views.py +++ b/custom_slack_provider/views.py @@ -1,7 +1,7 @@ import logging import requests -from requests.auth import AuthBase +from custom_slack_provider.slack import CustomSlackClient from accounts.models import CustomUser from django.core.exceptions import PermissionDenied from requests import RequestException @@ -30,18 +30,6 @@ logger = logging.getLogger(__name__) -AUTHORIZATION_HEADER = {'Authorization': "Bearer %s"} - - -class AuthBearer(AuthBase): - """ Custom requests authentication class for header """ - def __init__(self, token): - self.token = token - - def __call__(self, request): - request.headers["authorization"] = "Bearer " + self.token - return request - class SlackOAuth2Adapter(OAuth2Adapter): provider_id = SlackProvider.id @@ -56,41 +44,16 @@ def complete_login(self, request, app, token, **kwargs): return self.get_provider().sociallogin_from_response(request, extra_data) - def get_identity(self, token): - resp = requests.get( - self.identity_url, - auth=AuthBearer(token), - ) - resp = resp.json() - - if not resp.get('ok'): - raise OAuth2Error(f'UserInfo Exception: {resp.get("error")}') - - return resp - - def get_user_info(self, token, userid): - user_info = requests.get( - self.user_detail_url, - params={'user': userid}, - auth=AuthBearer(token), - ) - user_info = user_info.json() - - if not user_info.get('ok'): - raise OAuth2Error(f'UserInfo Exception: {user_info.get("error")}') - - return user_info.get('user', {}) - def get_data(self, token): - # Verify the user first - resp = self.get_identity(token) + # Verify user's identity and retrieve userid + user_slack_client = CustomSlackClient(token) + resp = user_slack_client.get_identity() userid = resp.get('user', {}).get('id') - user_info = requests.get( - self.user_detail_url, - params={'token': settings.SLACK_BOT_TOKEN, 'user': userid} - ) - user_info = self.get_user_info(token, userid) + + bot_slack_client = CustomSlackClient(settings.SLACK_BOT_TOKEN) + user_info = bot_slack_client.get_user_info(userid) + display_name = user_info.get('profile', {}).get('display_name_normalized') teamid = resp.get('team').get('id') From 4fcabecd4f2e1f9daeecfdc7d955090145ca7e68 Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Wed, 26 Jan 2022 10:51:02 +0000 Subject: [PATCH 05/22] Updating field name --- hackathon/forms.py | 24 ++++++++++++++++--- .../migrations/0048_hackathon_channel_url.py | 18 ++++++++++++++ .../0049_hackathon_channel_admins.py | 21 ++++++++++++++++ .../migrations/0050_auto_20220120_1052.py | 24 +++++++++++++++++++ .../migrations/0051_auto_20220120_1053.py | 20 ++++++++++++++++ .../migrations/0052_auto_20220126_1049.py | 18 ++++++++++++++ hackathon/models.py | 7 +++++- 7 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 hackathon/migrations/0048_hackathon_channel_url.py create mode 100644 hackathon/migrations/0049_hackathon_channel_admins.py create mode 100644 hackathon/migrations/0050_auto_20220120_1052.py create mode 100644 hackathon/migrations/0051_auto_20220120_1053.py create mode 100644 hackathon/migrations/0052_auto_20220126_1049.py diff --git a/hackathon/forms.py b/hackathon/forms.py index 13f8a172..80e43c86 100644 --- a/hackathon/forms.py +++ b/hackathon/forms.py @@ -1,6 +1,8 @@ from django import forms +from easy_select2 import Select2Multiple from accounts.models import Organisation +from accounts.models import CustomUser as User from .models import Hackathon, HackProject, HackAward, HackTeam, \ HackProjectScoreCategory, HackAwardCategory, Event from .lists import STATUS_TYPES_CHOICES @@ -105,18 +107,34 @@ class HackathonForm(forms.ModelForm): widget=forms.TextInput({'type': 'number', 'placeholder': 'Leave empty for no max'}) ) - channel_prefix = forms.CharField( + channel_name = forms.CharField( + required=False, label="Channel Prefix", - required=True, widget=forms.TextInput(), ) + channel_url = forms.CharField( + required=False, + label="Channel Url", + widget=forms.TextInput(attrs={ + 'readonly': True, + }), + ) + + channel_admins = forms.ModelMultipleChoiceField( + label="Channel Admins", + required=False, + queryset=User.objects.all(), + widget=Select2Multiple(select2attrs={'width': '100%'}) + ) + class Meta: model = Hackathon fields = ['display_name', 'description', 'theme', 'start_date', 'end_date', 'status', 'organisation', 'score_categories', 'team_size', 'tag_line', 'is_public', 'max_participants', - 'allow_external_registrations', 'registration_form' + 'allow_external_registrations', 'registration_form', + 'channel_name', 'channel_url', 'channel_admins', ] def __init__(self, *args, **kwargs): diff --git a/hackathon/migrations/0048_hackathon_channel_url.py b/hackathon/migrations/0048_hackathon_channel_url.py new file mode 100644 index 00000000..dbd15487 --- /dev/null +++ b/hackathon/migrations/0048_hackathon_channel_url.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.13 on 2022-01-19 13:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hackathon', '0047_hackathon_channel_prefix'), + ] + + operations = [ + migrations.AddField( + model_name='hackathon', + name='channel_url', + field=models.CharField(blank=True, help_text='Url', max_length=255, null=True), + ), + ] diff --git a/hackathon/migrations/0049_hackathon_channel_admins.py b/hackathon/migrations/0049_hackathon_channel_admins.py new file mode 100644 index 00000000..47085b7f --- /dev/null +++ b/hackathon/migrations/0049_hackathon_channel_admins.py @@ -0,0 +1,21 @@ +# Generated by Django 3.1.13 on 2022-01-20 10:14 + +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), + ('hackathon', '0048_hackathon_channel_url'), + ] + + operations = [ + migrations.AddField( + model_name='hackathon', + name='channel_admins', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='administered_channels', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/hackathon/migrations/0050_auto_20220120_1052.py b/hackathon/migrations/0050_auto_20220120_1052.py new file mode 100644 index 00000000..91c361c7 --- /dev/null +++ b/hackathon/migrations/0050_auto_20220120_1052.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.13 on 2022-01-20 10:52 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('hackathon', '0049_hackathon_channel_admins'), + ] + + operations = [ + migrations.RemoveField( + model_name='hackathon', + name='channel_admins', + ), + migrations.AddField( + model_name='hackathon', + name='channel_admins', + field=models.ManyToManyField(blank=True, null=True, related_name='administered_channels', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/hackathon/migrations/0051_auto_20220120_1053.py b/hackathon/migrations/0051_auto_20220120_1053.py new file mode 100644 index 00000000..d2db033b --- /dev/null +++ b/hackathon/migrations/0051_auto_20220120_1053.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1.13 on 2022-01-20 10:53 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('hackathon', '0050_auto_20220120_1052'), + ] + + operations = [ + migrations.AlterField( + model_name='hackathon', + name='channel_admins', + field=models.ManyToManyField(blank=True, related_name='administered_channels', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/hackathon/migrations/0052_auto_20220126_1049.py b/hackathon/migrations/0052_auto_20220126_1049.py new file mode 100644 index 00000000..dbf95559 --- /dev/null +++ b/hackathon/migrations/0052_auto_20220126_1049.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.13 on 2022-01-26 10:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('hackathon', '0051_auto_20220120_1053'), + ] + + operations = [ + migrations.RenameField( + model_name='hackathon', + old_name='channel_prefix', + new_name='channel_name', + ), + ] diff --git a/hackathon/models.py b/hackathon/models.py index 2d312764..7eac15b2 100644 --- a/hackathon/models.py +++ b/hackathon/models.py @@ -87,9 +87,14 @@ class Hackathon(models.Model): blank=True, help_text=("Link to the Google Form for registrations.") ) - channel_prefix = models.CharField( + channel_name = models.CharField( max_length=255, null=True, blank=True, help_text=("Only use lowercase and dash ('-') for spaces")) + channel_url = models.CharField( + max_length=255, null=True, blank=True, + help_text=("Url")) + channel_admins = models.ManyToManyField( + User, blank=True, related_name="administered_channels") def __str__(self): return self.display_name From 8d4b0ef6350dae7634090d21c10fdbf96e8936ae Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Wed, 26 Jan 2022 10:54:32 +0000 Subject: [PATCH 06/22] updating form with new field name --- hackathon/forms.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hackathon/forms.py b/hackathon/forms.py index 80e43c86..55ba18a9 100644 --- a/hackathon/forms.py +++ b/hackathon/forms.py @@ -109,8 +109,9 @@ class HackathonForm(forms.ModelForm): channel_name = forms.CharField( required=False, - label="Channel Prefix", + label="Channel Name", widget=forms.TextInput(), + help_text="Only use lower case letters, numbers and hyphen (-)" ) channel_url = forms.CharField( From c1a32d793a5520178c47fbc0c4d344e05d4a9d6c Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Wed, 26 Jan 2022 10:56:40 +0000 Subject: [PATCH 07/22] Updating views to create channel --- .../templates/hackathon/create-event.html | 26 +++++++++++++++++-- hackathon/views.py | 19 +++++++++++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/hackathon/templates/hackathon/create-event.html b/hackathon/templates/hackathon/create-event.html index c9999ea0..fff9a205 100644 --- a/hackathon/templates/hackathon/create-event.html +++ b/hackathon/templates/hackathon/create-event.html @@ -4,9 +4,14 @@ {% block css %} - + {{ form.media.css }} + + + + {{ form.media.js }} {% endblock %} {% block content %} @@ -42,8 +47,24 @@

Create Hackathon

{% if slack_enabled %}
- {{ form.channel_prefix|as_crispy_field }} + {{ form.channel_name|as_crispy_field }}
+
+
+ + {{form.channel_admins }} + These users will be added to the channel automatically. +
+
+ {% if hackathon_id and form.channel_url.value %} +
+ {{form.channel_url|as_crispy_field }} +
+ {% else %} +
+ {{ form.channel_url.as_hidden }} +
+ {% endif %} {% endif %}
{{ form.team_size|as_crispy_field }} @@ -84,6 +105,7 @@

Create Hackathon

{% endblock %} {% block js %} + diff --git a/hackathon/views.py b/hackathon/views.py index 8c425754..de117d18 100644 --- a/hackathon/views.py +++ b/hackathon/views.py @@ -23,7 +23,7 @@ HackAwardForm, HackTeamForm, EventForm from .lists import AWARD_CATEGORIES from .helpers import format_date, query_scores, create_judges_scores_table -from .tasks import send_email_from_template +from .tasks import send_email_from_template, create_new_slack_channel from accounts.models import UserType from accounts.decorators import can_access, has_access_to_hackathon @@ -283,7 +283,15 @@ def create_hackathon(request): hackathon_name = form.instance.display_name # Save the form - form.save() + hackathon = form.save() + + # Create a new slack channel for hackathon + # if the channel_name is filled in + if hackathon.channel_name: + create_new_slack_channel.apply_async(kwargs={ + 'channel_name': hackathon.channel_name, + 'hackathon_id': hackathon.id + }) # Taking the first 3 award categories and creating them for the # newly created hackathon. hack_award_categories = HackAwardCategory.objects.filter( @@ -310,6 +318,7 @@ def create_hackathon(request): def update_hackathon(request, hackathon_id): """ Allow users to edit hackathon event """ hackathon = get_object_or_404(Hackathon, pk=hackathon_id) + channel_name = hackathon.channel_name if request.method == 'GET': form = HackathonForm(instance=hackathon) @@ -324,7 +333,6 @@ def update_hackathon(request, hackathon_id): else: form = HackathonForm(request.POST, instance=hackathon) # Convert start and end date strings to datetime and validate - start_date = format_date(request.POST.get('start_date')) end_date = format_date(request.POST.get('end_date')) now = datetime.now() @@ -347,6 +355,11 @@ def update_hackathon(request, hackathon_id): if form.is_valid(): form.instance.updated = now form.save() + if channel_name != request.POST.get('channel_name'): + create_new_slack_channel.apply_async(kwargs={ + 'channel_name': hackathon.channel_name, + 'hackathon_id': hackathon.id + }) messages.success( request, (f'Thanks, {hackathon.display_name} has been ' f'successfully updated!')) From 3619ad13285af9b858fb21f13c89b15aff3782e4 Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Wed, 26 Jan 2022 10:57:02 +0000 Subject: [PATCH 08/22] Updating slack provider with channel creation logic and tests --- custom_slack_provider/slack.py | 18 ++++++++++--- custom_slack_provider/tests.py | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/custom_slack_provider/slack.py b/custom_slack_provider/slack.py index 2077812d..afe3ef0a 100644 --- a/custom_slack_provider/slack.py +++ b/custom_slack_provider/slack.py @@ -1,3 +1,4 @@ +import json import requests from requests.auth import AuthBase @@ -16,12 +17,13 @@ def __call__(self, request): class SlackException(Exception): def __init__(self, msg): - raise Exception(msg) + self.message = msg class CustomSlackClient(): identity_url = 'https://slack.com/api/users.identity' user_detail_url = 'https://slack.com/api/users.info' + create_conversation_url = 'https://slack.com/api/conversations.create' def __init__(self, token): self.token = token @@ -59,6 +61,16 @@ def create_slack_channel(self, channel_name, is_private=True): "is_private": is_private, "team_id": settings.SLACK_TEAM_ID, } - new_channel = self._make_slack_post_request(self.user_detail_url, - data=data) + new_channel = self._make_slack_post_request( + self.create_conversation_url, data=data) return new_channel.get('channel', {}) + + @staticmethod + def trigger_welcome_workflow(data): + res = requests.post(settings.SLACK_WELCOME_WORKFLOW_WEBHOOK, + data=json.dumps(data)) + + # A 200 response has an empty body + if res.status_code == 200: + return {'ok': True} + return res.json() diff --git a/custom_slack_provider/tests.py b/custom_slack_provider/tests.py index 58bb8875..0a24a2c9 100644 --- a/custom_slack_provider/tests.py +++ b/custom_slack_provider/tests.py @@ -1,8 +1,11 @@ from allauth.socialaccount.tests import OAuth2TestsMixin from allauth.tests import MockedResponse, TestCase from allauth.socialaccount.tests import setup_app +from requests import Response +from unittest.mock import patch, Mock from .provider import SlackProvider +from custom_slack_provider.slack import CustomSlackClient, SlackException from django.core.management import call_command @@ -22,3 +25,49 @@ def get_mocked_response(self): "team_id": "T12345", "user_id": "U12345" }""") # noqa + + +class SlackClientTest(TestCase): + def setUp(self): + self.token = 'TEST' + + @patch('requests.get') + def test__make_slack_get_request(self, get): + mock_response = Mock() + mock_response.json.return_value = {'ok': True} + get.return_value = mock_response + client = CustomSlackClient(self.token) + response = client._make_slack_get_request(url='test') + self.assertTrue(response['ok']) + + mock_response.json.return_value = {'ok': False, + 'error': 'Slack Error'} + try: + client._make_slack_get_request(url='test') + except SlackException as e: + self.assertTrue(isinstance(e, SlackException)) + self.assertEquals(e.message, 'Slack Error') + + @patch('requests.post') + def test__make_slack_post_request(self, post): + mock_response = Mock() + mock_response.json.return_value = {'ok': True} + post.return_value = mock_response + client = CustomSlackClient(self.token) + response = client._make_slack_post_request(url='test', data={}) + self.assertTrue(response['ok']) + + mock_response.json.return_value = {'ok': False, + 'error': 'Slack Error'} + try: + client._make_slack_get_request(url='test') + except SlackException as e: + self.assertTrue(isinstance(e, SlackException)) + self.assertEquals(e.message, 'Slack Error') + + @patch('custom_slack_provider.slack.CustomSlackClient._make_slack_get_request') + def test_get_identity(self, _make_slack_get_request): + _make_slack_get_request.return_value = {'user': {'id': 1}} + client = CustomSlackClient(self.token) + response = client.get_identity() + self.assertEqual(response['user']['id'], 1) From be85a4748bd36ecf3a3c47984b14f6b7eb3d4361 Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Wed, 26 Jan 2022 10:58:31 +0000 Subject: [PATCH 09/22] Remove reference to welcome workflow --- custom_slack_provider/slack.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/custom_slack_provider/slack.py b/custom_slack_provider/slack.py index afe3ef0a..0838449f 100644 --- a/custom_slack_provider/slack.py +++ b/custom_slack_provider/slack.py @@ -64,13 +64,3 @@ def create_slack_channel(self, channel_name, is_private=True): new_channel = self._make_slack_post_request( self.create_conversation_url, data=data) return new_channel.get('channel', {}) - - @staticmethod - def trigger_welcome_workflow(data): - res = requests.post(settings.SLACK_WELCOME_WORKFLOW_WEBHOOK, - data=json.dumps(data)) - - # A 200 response has an empty body - if res.status_code == 200: - return {'ok': True} - return res.json() From 684ca305b975f6b86ff6f0d7bd0976375d3ccdf7 Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Wed, 26 Jan 2022 10:59:02 +0000 Subject: [PATCH 10/22] Updated settings and requirements --- main/settings.py | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/main/settings.py b/main/settings.py index c292e0ab..1ee4931a 100644 --- a/main/settings.py +++ b/main/settings.py @@ -39,6 +39,7 @@ "django_celery_results", "crispy_forms", "django_celery_results", + "easy_select2", # custom apps "accounts", diff --git a/requirements.txt b/requirements.txt index f0b78f3d..13709d2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ django-allauth==0.42.0 django-celery-beat==2.2.1 django-celery-results==2.2.0 django-crispy-forms==1.9.2 +django-easy-select2==1.5.8 django-extensions==3.1.0 graphviz==0.16 gunicorn==20.0.4 From 054155a04b9eef73bbf063f142ce05b7147054f6 Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Wed, 26 Jan 2022 11:09:19 +0000 Subject: [PATCH 11/22] Updating settings --- accounts/forms.py | 4 ++-- docker-compose.yml | 12 +----------- hackathon/tasks.py | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/accounts/forms.py b/accounts/forms.py index c9bf2ba8..ac790ec6 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -53,11 +53,11 @@ class EditProfileForm(forms.ModelForm): full_name = forms.CharField( max_length=30, widget=forms.TextInput(attrs={'placeholder': 'Full Name'}), - label='') + label='Name') slack_display_name = forms.CharField( max_length=30, widget=forms.TextInput(attrs={'placeholder': 'Slack Display Name'}), - label='') + label='Slack Display Name') current_lms_module = forms.CharField( widget=forms.Select(choices=LMS_MODULES_CHOICES), label="Where are you currently in the programme?" diff --git a/docker-compose.yml b/docker-compose.yml index ba8bdb1c..f30b5652 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,7 +25,7 @@ services: environment: - ENV_FILE=/hackathon-app/.env - - DEVELOPMENT=1 + - STATIC_URL=https://codeinstitute-webpublic.s3.eu-west-1.amazonaws.com/hackathon_staticfiles/1.62-a/ entrypoint: ['python3', 'manage.py', 'runserver', '0.0.0.0:8000'] ports: - "8000:8000" @@ -64,13 +64,3 @@ services: image: redis:6.2.4 ports: - "6379:6379" - - worker: - <<: *hackathon-app-main - entrypoint: ["celery", "-A", "main", "worker", "-l", "INFO"] - ports: [] - - # environment: - # - ENV_FILE=/hackathon-app/.env - # volumes: - # - ./.env:/hackathon-app/.env diff --git a/hackathon/tasks.py b/hackathon/tasks.py index 600d1ea2..134f2aa2 100644 --- a/hackathon/tasks.py +++ b/hackathon/tasks.py @@ -9,7 +9,15 @@ from accounts.models import EmailTemplate, SlackSiteSettings +from celery import shared_task +from django.conf import settings + +from accounts.models import CustomUser as User +from hackathon.models import Hackathon +from custom_slack_provider.slack import CustomSlackClient + logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) @shared_task @@ -34,3 +42,34 @@ def send_email_from_template(user_email, user_name, hackathon_display_name, temp "Please create it on the Django Admin Panel")) except SMTPException: logger.exception("There was an issue sending the email.") +def log_user_numbers(): + users = User.objects.count() + logger.info(f'Number of users currently: {users}') + return + + +@shared_task +def create_new_slack_channel(hackathon_id, channel_name): + """ Create a new Slack Channel/Conversation in an existing Workspace """ + if not settings.SLACK_ENABLED: + logger.info("Slack not enabled.") + return + + hackathon = Hackathon.objects.get(id=hackathon_id) + logger.info( + (f"Creating new Slack channel {channel_name} for hackathon " + f"{hackathon.display_name} in Slack Workspace " + f"{settings.SLACK_WORKSPACE}({settings.SLACK_TEAM_ID})")) + slack_client = CustomSlackClient(settings.SLACK_BOT_TOKEN) + channel_id = slack_client.create_slack_channel( + channel_name, is_private=True) + logger.info(f"Channel with id {channel_id} created.") + + if not channel_id: + logger.error("No Channel Id found.") + return + + channel_url = f'https://{settings.SLACK_WORKSPACE}.slack.com/archives/{channel_id}' # noqa: E501 + hackathon.channel_url = channel_url + hackathon.save() + logger.info(f"Hackathon {hackathon.display_name} updated successfully.") From 7e674906c24fda53cb777e53449fae4dccff801e Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Thu, 4 Aug 2022 19:54:32 +0100 Subject: [PATCH 12/22] adding slack channel creation (WIP) --- custom_slack_provider/slack.py | 20 ++++++++++++++- custom_slack_provider/tests.py | 28 ++++++++++++++++++-- hackathon/tasks.py | 14 ++++++++-- hackathon/tests/task_tests.py | 47 +++++++++++++++++++++++++++++++--- hackathon/tests/test_models.py | 2 +- requirements.txt | 1 + 6 files changed, 103 insertions(+), 9 deletions(-) diff --git a/custom_slack_provider/slack.py b/custom_slack_provider/slack.py index 0838449f..7592a819 100644 --- a/custom_slack_provider/slack.py +++ b/custom_slack_provider/slack.py @@ -1,4 +1,4 @@ -import json +import re import requests from requests.auth import AuthBase @@ -24,6 +24,7 @@ class CustomSlackClient(): identity_url = 'https://slack.com/api/users.identity' user_detail_url = 'https://slack.com/api/users.info' create_conversation_url = 'https://slack.com/api/conversations.create' + invite_conversation_url = 'https://slack.com/api/conversations.invite' def __init__(self, token): self.token = token @@ -64,3 +65,20 @@ def create_slack_channel(self, channel_name, is_private=True): new_channel = self._make_slack_post_request( self.create_conversation_url, data=data) return new_channel.get('channel', {}) + + def _extract_userid_from_username(self, username): + """ Extracts the Slack userid from a hackathon platform userid + when Slack is enabled and the account was created with a valid userid + schema: [SLACK_USER_ID]_[WORKSPACE_TEAM_ID]""" + if not re.match(r'[A-Z0-9]*[_]T[A-Z0-9]*', username): + raise SlackException('Error adding user to channel') + return username.split('_')[0] + + def add_user_to_slack_channel(self, username, channel_id): + data = { + "user": self._extract_userid_from_username(username), + "channel": channel_id, + } + user_added = self._make_slack_post_request( + self.invite_conversation_url, data=data) + return user_added diff --git a/custom_slack_provider/tests.py b/custom_slack_provider/tests.py index 0a24a2c9..dfc92def 100644 --- a/custom_slack_provider/tests.py +++ b/custom_slack_provider/tests.py @@ -7,6 +7,7 @@ from .provider import SlackProvider from custom_slack_provider.slack import CustomSlackClient, SlackException from django.core.management import call_command +from class SlackOAuth2Tests(OAuth2TestsMixin, TestCase): @@ -60,14 +61,37 @@ def test__make_slack_post_request(self, post): mock_response.json.return_value = {'ok': False, 'error': 'Slack Error'} try: - client._make_slack_get_request(url='test') + client._make_slack_post_request(url='test', data={}) except SlackException as e: self.assertTrue(isinstance(e, SlackException)) self.assertEquals(e.message, 'Slack Error') - @patch('custom_slack_provider.slack.CustomSlackClient._make_slack_get_request') + @patch('custom_slack_provider.slack.CustomSlackClient._make_slack_get_request') # noqa: 501 def test_get_identity(self, _make_slack_get_request): _make_slack_get_request.return_value = {'user': {'id': 1}} client = CustomSlackClient(self.token) response = client.get_identity() self.assertEqual(response['user']['id'], 1) + + def test__extract_userid_from_username(self): + valid_username = 'US123123_T123123' + invalid_username = 'bob@bob.com' + client = CustomSlackClient(self.token) + userid = client._extract_userid_from_username(valid_username) + self.assertEqual(userid, 'US123123') + try: + userid = client._extract_userid_from_username(invalid_username) + except SlackException as e: + self.assertTrue(isinstance(e, SlackException)) + self.assertEquals(e.message, 'Error adding user to channel') + + @patch('custom_slack_provider.slack.CustomSlackClient._make_slack_post_request') # noqa: 501 + def test_add_user_to_slack_channel(self, _make_slack_post_request): + _make_slack_post_request.return_value = { + 'ok': True, + 'channel': {'id': 'CH123123'} + } + client = CustomSlackClient(self.token) + response = client.add_user_to_slack_channel(username='UA123123_T15666', + channel_id='CH123123') + self.assertEqual(response['channel']['id'], 'CH123123') diff --git a/hackathon/tasks.py b/hackathon/tasks.py index 134f2aa2..e84ec0f5 100644 --- a/hackathon/tasks.py +++ b/hackathon/tasks.py @@ -14,7 +14,7 @@ from accounts.models import CustomUser as User from hackathon.models import Hackathon -from custom_slack_provider.slack import CustomSlackClient +from custom_slack_provider.slack import CustomSlackClient, SlackException logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) @@ -61,8 +61,10 @@ def create_new_slack_channel(hackathon_id, channel_name): f"{hackathon.display_name} in Slack Workspace " f"{settings.SLACK_WORKSPACE}({settings.SLACK_TEAM_ID})")) slack_client = CustomSlackClient(settings.SLACK_BOT_TOKEN) - channel_id = slack_client.create_slack_channel( + channel = slack_client.create_slack_channel( channel_name, is_private=True) + + channel_id = channel.get('id') logger.info(f"Channel with id {channel_id} created.") if not channel_id: @@ -73,3 +75,11 @@ def create_new_slack_channel(hackathon_id, channel_name): hackathon.channel_url = channel_url hackathon.save() logger.info(f"Hackathon {hackathon.display_name} updated successfully.") + + logger.info("Adding channel admins") + for admin in hackathon.channel_admins.all(): + try: + slack_client.add_user_to_slack_channel(admin.username, channel_id) + except SlackException: + logger.exception((f"Could not add user with id {admin.id} " + f"to channel {channel_id}.")) diff --git a/hackathon/tests/task_tests.py b/hackathon/tests/task_tests.py index 2a342d40..e4d41b5e 100644 --- a/hackathon/tests/task_tests.py +++ b/hackathon/tests/task_tests.py @@ -1,5 +1,46 @@ -from django.test import TestCase +from datetime import datetime + +from django.conf import settings +from django.test import TestCase, override_settings +import responses +from unittest.mock import patch, Mock + +from accounts.models import Organisation, CustomUser as User +from hackathon.models import Hackathon +from hackathon.tasks import create_new_slack_channel + class TaskTests(TestCase): - def test_simple_task(self): - pass \ No newline at end of file + def setUp(self): + organisation = Organisation.objects.create() + self.user = User.objects.create( + username="U213123_T123123", + slack_display_name="bob", + organisation=organisation, + ) + self.hackathon = Hackathon.objects.create( + created_by=self.user, + display_name="hacktest", + description="lorem ipsum", + start_date=f'{datetime.now()}', + end_date=f'{datetime.now()}') + self.hackathon.channel_admins.add(self.user) + + @override_settings(CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, + CELERY_ALWAYS_EAGER=True, + BROKER_BACKEND='memory') + def test_create_new_slack_channel(self): + channel_id = 'CH123123' + responses.add( + responses.POST, 'https://slack.com/api/conversations.create', + json={'ok': True, 'channel': {'id': channel_id}}, status=200) + responses.add( + responses.POST, 'https://slack.com/api/conversations.invite', + json={'ok': True}, status=200) + create_new_slack_channel.apply_async(args=[ + self.hackathon.id, self.user.username]) + + import time; time.sleep(3) + self.assertEquals( + self.hackathon.channel_url, + f'https://{settings.SLACK_WORKSPACE}.slack.com/archives/{channel_id}') diff --git a/hackathon/tests/test_models.py b/hackathon/tests/test_models.py index 206fce38..6cea2dfc 100644 --- a/hackathon/tests/test_models.py +++ b/hackathon/tests/test_models.py @@ -36,7 +36,7 @@ def setUp(self): created_by=user, display_name="testaward", description="lorem ipsum") - + hack_award = HackAward.objects.create( created_by=user, hack_award_category=award_category, diff --git a/requirements.txt b/requirements.txt index 13709d2b..001d260f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -52,6 +52,7 @@ pytz==2020.4 redis==4.1.1 requests==2.24.0 requests-oauthlib==1.3.0 +responses==0.17.0 retrying==1.3.3 sentry-sdk==0.10.2 six==1.15.0 From 3144158b2fe2461d276252aa8961ea5961d26208 Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Mon, 23 Jan 2023 12:13:58 +0000 Subject: [PATCH 13/22] Fixing rebase issues --- hackathon/tasks.py | 4 ---- main/settings.py | 1 - 2 files changed, 5 deletions(-) diff --git a/hackathon/tasks.py b/hackathon/tasks.py index e84ec0f5..965204c7 100644 --- a/hackathon/tasks.py +++ b/hackathon/tasks.py @@ -42,10 +42,6 @@ def send_email_from_template(user_email, user_name, hackathon_display_name, temp "Please create it on the Django Admin Panel")) except SMTPException: logger.exception("There was an issue sending the email.") -def log_user_numbers(): - users = User.objects.count() - logger.info(f'Number of users currently: {users}') - return @shared_task diff --git a/main/settings.py b/main/settings.py index 1ee4931a..7286a40e 100644 --- a/main/settings.py +++ b/main/settings.py @@ -38,7 +38,6 @@ "django_celery_beat", "django_celery_results", "crispy_forms", - "django_celery_results", "easy_select2", # custom apps From a32b35f283bcf540a508d82beea3e82e132146a0 Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Mon, 23 Jan 2023 14:58:35 +0000 Subject: [PATCH 14/22] Refactoring slack channel creation --- docker-compose.yml | 20 +++++++++++ .../0047_hackathon_channel_prefix.py | 18 ---------- .../migrations/0048_hackathon_channel_url.py | 18 ---------- .../migrations/0049_auto_20230123_1219.py | 30 +++++++++++++++++ .../0049_hackathon_channel_admins.py | 21 ------------ .../migrations/0050_auto_20220120_1052.py | 24 -------------- .../migrations/0051_auto_20220120_1053.py | 20 ----------- .../migrations/0052_auto_20220126_1049.py | 18 ---------- teams/helpers.py | 33 +++++++++++++++++-- teams/views.py | 32 +++++------------- 10 files changed, 89 insertions(+), 145 deletions(-) delete mode 100644 hackathon/migrations/0047_hackathon_channel_prefix.py delete mode 100644 hackathon/migrations/0048_hackathon_channel_url.py create mode 100644 hackathon/migrations/0049_auto_20230123_1219.py delete mode 100644 hackathon/migrations/0049_hackathon_channel_admins.py delete mode 100644 hackathon/migrations/0050_auto_20220120_1052.py delete mode 100644 hackathon/migrations/0051_auto_20220120_1053.py delete mode 100644 hackathon/migrations/0052_auto_20220126_1049.py diff --git a/docker-compose.yml b/docker-compose.yml index f30b5652..4792e1f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,7 @@ services: environment: - ENV_FILE=/hackathon-app/.env + - DEVELOPMENT=1 - STATIC_URL=https://codeinstitute-webpublic.s3.eu-west-1.amazonaws.com/hackathon_staticfiles/1.62-a/ entrypoint: ['python3', 'manage.py', 'runserver', '0.0.0.0:8000'] ports: @@ -37,11 +38,30 @@ services: environment: - ENV_FILE=/hackathon-app/.env - DEVELOPMENT=1 + - STATIC_URL=https://codeinstitute-webpublic.s3.eu-west-1.amazonaws.com/hackathon_staticfiles/1.62-a/ entrypoint: ["celery", "-A", "main", "worker", "-l", "info"] volumes: + - ./staticfiles/:/hackathon-app/staticfiles/ - ./data/:/hackathon-app/data/ - ./.env:/hackathon-app/.env + # 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/ + - ./home/:/hackathon-app/home/ + - ./images/:/hackathon-app/images/ + - ./main/:/hackathon-app/main/ + - ./profiles/:/hackathon-app/profiles/ + - ./resources/:/hackathon-app/resources/ + - ./showcase/:/hackathon-app/showcase/ + - ./submissions/:/hackathon-app/submissions/ + - ./teams/:/hackathon-app/teams/ + - ./templates/:/hackathon-app/templates/ + - ./static/:/hackathon-app/static/ + mysql: image: docker.io/mysql:5.6.36 command: mysqld --character-set-server=utf8 --collation-server=utf8_general_ci diff --git a/hackathon/migrations/0047_hackathon_channel_prefix.py b/hackathon/migrations/0047_hackathon_channel_prefix.py deleted file mode 100644 index d18a5963..00000000 --- a/hackathon/migrations/0047_hackathon_channel_prefix.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1.13 on 2022-01-17 14:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('hackathon', '0046_auto_20220113_1350'), - ] - - operations = [ - migrations.AddField( - model_name='hackathon', - name='channel_prefix', - field=models.CharField(blank=True, help_text="Only use lowercase and dash ('-') for spaces", max_length=255, null=True), - ), - ] diff --git a/hackathon/migrations/0048_hackathon_channel_url.py b/hackathon/migrations/0048_hackathon_channel_url.py deleted file mode 100644 index dbd15487..00000000 --- a/hackathon/migrations/0048_hackathon_channel_url.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1.13 on 2022-01-19 13:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('hackathon', '0047_hackathon_channel_prefix'), - ] - - operations = [ - migrations.AddField( - model_name='hackathon', - name='channel_url', - field=models.CharField(blank=True, help_text='Url', max_length=255, null=True), - ), - ] diff --git a/hackathon/migrations/0049_auto_20230123_1219.py b/hackathon/migrations/0049_auto_20230123_1219.py new file mode 100644 index 00000000..c71bc8a0 --- /dev/null +++ b/hackathon/migrations/0049_auto_20230123_1219.py @@ -0,0 +1,30 @@ +# Generated by Django 3.1.13 on 2023-01-23 12:19 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('hackathon', '0048_auto_20221219_1655'), + ] + + operations = [ + migrations.AddField( + model_name='hackathon', + name='channel_admins', + field=models.ManyToManyField(blank=True, related_name='administered_channels', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='hackathon', + name='channel_name', + field=models.CharField(blank=True, help_text="Only use lowercase and dash ('-') for spaces", max_length=255, null=True), + ), + migrations.AddField( + model_name='hackathon', + name='channel_url', + field=models.CharField(blank=True, help_text='Url', max_length=255, null=True), + ), + ] diff --git a/hackathon/migrations/0049_hackathon_channel_admins.py b/hackathon/migrations/0049_hackathon_channel_admins.py deleted file mode 100644 index 47085b7f..00000000 --- a/hackathon/migrations/0049_hackathon_channel_admins.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.1.13 on 2022-01-20 10:14 - -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), - ('hackathon', '0048_hackathon_channel_url'), - ] - - operations = [ - migrations.AddField( - model_name='hackathon', - name='channel_admins', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='administered_channels', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/hackathon/migrations/0050_auto_20220120_1052.py b/hackathon/migrations/0050_auto_20220120_1052.py deleted file mode 100644 index 91c361c7..00000000 --- a/hackathon/migrations/0050_auto_20220120_1052.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.1.13 on 2022-01-20 10:52 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('hackathon', '0049_hackathon_channel_admins'), - ] - - operations = [ - migrations.RemoveField( - model_name='hackathon', - name='channel_admins', - ), - migrations.AddField( - model_name='hackathon', - name='channel_admins', - field=models.ManyToManyField(blank=True, null=True, related_name='administered_channels', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/hackathon/migrations/0051_auto_20220120_1053.py b/hackathon/migrations/0051_auto_20220120_1053.py deleted file mode 100644 index d2db033b..00000000 --- a/hackathon/migrations/0051_auto_20220120_1053.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.1.13 on 2022-01-20 10:53 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('hackathon', '0050_auto_20220120_1052'), - ] - - operations = [ - migrations.AlterField( - model_name='hackathon', - name='channel_admins', - field=models.ManyToManyField(blank=True, related_name='administered_channels', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/hackathon/migrations/0052_auto_20220126_1049.py b/hackathon/migrations/0052_auto_20220126_1049.py deleted file mode 100644 index dbf95559..00000000 --- a/hackathon/migrations/0052_auto_20220126_1049.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1.13 on 2022-01-26 10:49 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('hackathon', '0051_auto_20220120_1053'), - ] - - operations = [ - migrations.RenameField( - model_name='hackathon', - old_name='channel_prefix', - new_name='channel_name', - ), - ] diff --git a/teams/helpers.py b/teams/helpers.py index b7109acc..8f7adcd5 100644 --- a/teams/helpers.py +++ b/teams/helpers.py @@ -234,15 +234,42 @@ def calculate_timezone_offset(timezone, timezone_offset): return offset - timezone_offset +def create_slack_channel(endpoint, headers, params): + create_response = requests.get(endpoint, params=params, headers=headers) + if create_response.status_code != 200: + return { + 'ok': False, + 'error': ('An error occurred creating the Private Slack Channel. ' + f'Error code: {create_response.get("error")}') + } + + create_response = create_response.json() + if not create_response.get('ok'): + if create_response.get('error') == 'name_taken': + error_msg = (f'An error occurred creating the Private Slack Channel. ' + f'A channel with the name "{params["name"]}" already ' + f'exists. Please change your team name and try again ' + f'or contact an administrator') + else: + error_msg = (f'An error occurred creating the Private Slack Channel. ' + f'Error code: {create_response.get("error")}') + return { + 'ok': False, + 'error': error_msg + } + + return create_response + + def invite_users_to_slack_channel(endpoint, headers, params): response = requests.post(endpoint, params=params, headers=headers) - if not response.status_code == 200: + if response.status_code != 200: return { 'ok': False, 'error': ('An unexpected error occurred creating the ' 'Private Slack Channel.') } - + response = response.json() if not response.get('ok'): return { @@ -250,5 +277,5 @@ def invite_users_to_slack_channel(endpoint, headers, params): 'error': ('An error occurred creating the Private Slack Channel. ' f'Error code: {response.get("error")}') } - + return {'ok': True, 'response': response} diff --git a/teams/views.py b/teams/views.py index 10d2c5de..8af879ce 100644 --- a/teams/views.py +++ b/teams/views.py @@ -20,7 +20,8 @@ choose_team_levels, find_all_combinations, distribute_participants_to_teams, create_teams_in_view, update_team_participants, - calculate_timezone_offset, invite_users_to_slack_channel) + calculate_timezone_offset, invite_users_to_slack_channel, + create_slack_channel) from teams.forms import HackProjectForm, EditTeamName SLACK_CHANNEL_ENDPOINT = 'https://slack.com/api/conversations.create' @@ -246,9 +247,9 @@ def create_private_channel(request, team_id): return redirect(reverse('view_team', kwargs={'team_id': team_id})) # Create new channel - date_str = datetime.now().strftime('%y%m') + date_str = datetime.now().strftime('%b-%y').lower() team_name = re.sub('[^A-Za-z0-9]+', '', team.display_name.lower()) - channel_name = f'{date_str}-hackathon-{team_name}' + channel_name = f'{date_str}-hackathon-{team.hackathon.id}-{team_name}' params = { 'team_id': settings.SLACK_TEAM_ID, 'name': channel_name, @@ -257,27 +258,12 @@ def create_private_channel(request, team_id): # Cannot use Bot Token to create a channel if workspace settings # specify only Admins and Owners can create channels headers = {'Authorization': f'Bearer {settings.SLACK_ADMIN_TOKEN}'} - create_response = requests.get(SLACK_CHANNEL_ENDPOINT, params=params, - headers=headers) - if not create_response.status_code == 200: - messages.error(request, (f'An error occurred creating the Private Slack Channel. ' - f'Error code: {create_response.get("error")}')) - return redirect(reverse('view_team', kwargs={'team_id': team_id})) - - create_response = create_response.json() - if not create_response.get('ok'): - if create_response.get('error') == 'name_taken': - error_msg = (f'An error occurred creating the Private Slack Channel. ' - f'A channel with the name "{channel_name}" already ' - f'exists. Please change your team name and try again ' - f'or contact an administrator') - else: - error_msg = (f'An error occurred creating the Private Slack Channel. ' - f'Error code: {create_response.get("error")}') - messages.error(request, error_msg) + channel_response = create_slack_channel(SLACK_CHANNEL_ENDPOINT, headers, params) + if not response['ok']: + messages.error(request, response['error']) return redirect(reverse('view_team', kwargs={'team_id': team_id})) - - channel = create_response.get('channel', {}).get('id') + + channel = channel_response.get('channel', {}).get('id') communication_channel = (f'https://{settings.SLACK_WORKSPACE}.slack.com/' f'app_redirect?channel={channel}') team.communication_channel = communication_channel From ecfc4b508b323ae86c7600cb8342a41451890437 Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Mon, 24 Jul 2023 09:34:14 +0100 Subject: [PATCH 15/22] Updating channel creation to remove Slack admin if wanted (WIP) --- ...ksitesettings_remove_admin_from_channel.py | 18 ++++ accounts/models.py | 6 ++ custom_slack_provider/slack.py | 87 +++++++++++++++--- custom_slack_provider/tests.py | 9 +- hackathon/.views.py.swp | Bin 0 -> 36864 bytes hackathon/tasks.py | 81 +++++++++++----- hackathon/tests/task_tests.py | 6 +- hackathon/views.py | 27 +++++- teams/helpers.py | 47 ---------- teams/views.py | 56 ++++++----- 10 files changed, 211 insertions(+), 126 deletions(-) create mode 100644 accounts/migrations/0021_slacksitesettings_remove_admin_from_channel.py create mode 100644 hackathon/.views.py.swp diff --git a/accounts/migrations/0021_slacksitesettings_remove_admin_from_channel.py b/accounts/migrations/0021_slacksitesettings_remove_admin_from_channel.py new file mode 100644 index 00000000..6d96394a --- /dev/null +++ b/accounts/migrations/0021_slacksitesettings_remove_admin_from_channel.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.13 on 2023-01-23 16:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0020_auto_20230104_1655'), + ] + + operations = [ + migrations.AddField( + model_name='slacksitesettings', + name='remove_admin_from_channel', + field=models.BooleanField(default=True, help_text='The user linked to the ADMIN_BOT_TOKEN will automatically be added to any new channels. If this is ticked, the user will be removed from private team channels if they are not part of the team, facilitator or the slack admins'), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 970e3f4f..0ffc433f 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -203,6 +203,12 @@ class SlackSiteSettings(SingletonModel): communication_channel_type = models.CharField( max_length=50, choices=COMMUNICATION_CHANNEL_TYPES, default='slack_private_channel') + remove_admin_from_channel = models.BooleanField( + default=True, + help_text=("The user linked to the ADMIN_BOT_TOKEN will automatically " + "be added to any new channels. If this is ticked, the user " + "will be removed from private team channels if they are not " + "part of the team, facilitator or the slack admins")) def __str__(self): return "Slack Settings" diff --git a/custom_slack_provider/slack.py b/custom_slack_provider/slack.py index 7592a819..0953517b 100644 --- a/custom_slack_provider/slack.py +++ b/custom_slack_provider/slack.py @@ -1,8 +1,11 @@ +import logging import re import requests from requests.auth import AuthBase -from django.conf import settings + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) class AuthBearer(AuthBase): @@ -25,12 +28,16 @@ class CustomSlackClient(): user_detail_url = 'https://slack.com/api/users.info' create_conversation_url = 'https://slack.com/api/conversations.create' invite_conversation_url = 'https://slack.com/api/conversations.invite' + leave_conversation_url = 'https://slack.com/api/conversations.leave' def __init__(self, token): self.token = token def _make_slack_get_request(self, url, params=None): resp = requests.get(url, auth=AuthBearer(self.token), params=params) + if resp.status_code != 200: + raise SlackException(resp.get("error")) + resp = resp.json() if not resp.get('ok'): @@ -40,31 +47,60 @@ def _make_slack_get_request(self, url, params=None): def _make_slack_post_request(self, url, data): resp = requests.post(url, auth=AuthBearer(self.token), data=data) - resp = resp.json() - - if not resp.get('ok'): - raise SlackException(resp.get("error")) - - return resp + if resp.status_code != 200: + return { + 'ok': False, + 'error': resp.get("error") + } + return resp.json() def get_identity(self): return self._make_slack_get_request(self.identity_url) + def leave_channel(self, channel): + data = { + 'channel': channel + } + leave_channel = self._make_slack_post_request( + self.leave_conversation_url, data=data) + + if not leave_channel.get('ok'): + print(('An error occurred adding users to the Private Slack Channel. ' + f'Error code: {leave_channel.get("error")}')) + + return leave_channel + def get_user_info(self, userid): params = {"user": userid} response = self._make_slack_get_request(self.user_detail_url, params=params) return response.get('user', {}) - def create_slack_channel(self, channel_name, is_private=True): + def create_slack_channel(self, channel_name, team_id, is_private=True): data = { + "team_id": team_id, "name": channel_name, "is_private": is_private, - "team_id": settings.SLACK_TEAM_ID, } new_channel = self._make_slack_post_request( self.create_conversation_url, data=data) - return new_channel.get('channel', {}) + + if not new_channel.get('ok'): + if new_channel.get('error') == 'name_taken': + error_msg = (f'An error occurred creating the Private Slack Channel. ' + f'A channel with the name "{channel_name}" already ' + f'exists. Please change your team name and try again ' + f'or contact an administrator') + else: + error_msg = (f'An error occurred creating the Private Slack Channel. ' + f'Error code: {new_channel.get("error")}') + return { + 'ok': False, + 'error': error_msg + } + + logger.info(f"Successfully created {channel_name} ({new_channel.get('channel', {}).get('id')}).") + return new_channel def _extract_userid_from_username(self, username): """ Extracts the Slack userid from a hackathon platform userid @@ -74,11 +110,36 @@ def _extract_userid_from_username(self, username): raise SlackException('Error adding user to channel') return username.split('_')[0] - def add_user_to_slack_channel(self, username, channel_id): + def invite_users_to_slack_channel(self, users, channel): + data = { + "users": users, + "channel": channel, + } + user_added = self._make_slack_post_request( + self.invite_conversation_url, data=data) + + if not user_added.get('ok'): + return { + 'ok': False, + 'error': ('An error occurred adding users to Private Slack Channel {channel}. ' + f'Error code: {user_added.get("error")}') + } + + return user_added + + def kick_user_from_slack_channel(self, user, channel): data = { - "user": self._extract_userid_from_username(username), - "channel": channel_id, + "user": user, + "channel": channel, } user_added = self._make_slack_post_request( self.invite_conversation_url, data=data) + + if not user_added.get('ok'): + return { + 'ok': False, + 'error': (f'An error occurred kicking user {user} from Private Slack Channel {channel}. ' + f'Error code: {user_added.get("error")}') + } + return user_added diff --git a/custom_slack_provider/tests.py b/custom_slack_provider/tests.py index dfc92def..c1c1541f 100644 --- a/custom_slack_provider/tests.py +++ b/custom_slack_provider/tests.py @@ -7,7 +7,6 @@ from .provider import SlackProvider from custom_slack_provider.slack import CustomSlackClient, SlackException from django.core.management import call_command -from class SlackOAuth2Tests(OAuth2TestsMixin, TestCase): @@ -36,6 +35,7 @@ def setUp(self): def test__make_slack_get_request(self, get): mock_response = Mock() mock_response.json.return_value = {'ok': True} + mock_response.status_code = 200 get.return_value = mock_response client = CustomSlackClient(self.token) response = client._make_slack_get_request(url='test') @@ -53,6 +53,7 @@ def test__make_slack_get_request(self, get): def test__make_slack_post_request(self, post): mock_response = Mock() mock_response.json.return_value = {'ok': True} + mock_response.status_code = 200 post.return_value = mock_response client = CustomSlackClient(self.token) response = client._make_slack_post_request(url='test', data={}) @@ -86,12 +87,12 @@ def test__extract_userid_from_username(self): self.assertEquals(e.message, 'Error adding user to channel') @patch('custom_slack_provider.slack.CustomSlackClient._make_slack_post_request') # noqa: 501 - def test_add_user_to_slack_channel(self, _make_slack_post_request): + def test_invite_users_to_slack_channel(self, _make_slack_post_request): _make_slack_post_request.return_value = { 'ok': True, 'channel': {'id': 'CH123123'} } client = CustomSlackClient(self.token) - response = client.add_user_to_slack_channel(username='UA123123_T15666', - channel_id='CH123123') + response = client.invite_users_to_slack_channel(users='UA123123_T15666', + channel='CH123123') self.assertEqual(response['channel']['id'], 'CH123123') diff --git a/hackathon/.views.py.swp b/hackathon/.views.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..b7516d5bed5bd4ec1d025d9e8aa3e9ca08861f62 GIT binary patch literal 36864 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 Date: Fri, 20 Sep 2024 17:38:22 +0100 Subject: [PATCH 16/22] Removing unneccessary files from repo --- .gitignore | 5 ++++- hackathon/.views.py.swp | Bin 36864 -> 0 bytes 2 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 hackathon/.views.py.swp diff --git a/.gitignore b/.gitignore index c124af66..2e91606f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ MYNOTES.md staticfiles data/ todo.txt -node_modules/ \ No newline at end of file +node_modules/ +*.swp +*.swo +*.swn diff --git a/hackathon/.views.py.swp b/hackathon/.views.py.swp deleted file mode 100644 index b7516d5bed5bd4ec1d025d9e8aa3e9ca08861f62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36864 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 Date: Fri, 27 Sep 2024 16:36:44 +0100 Subject: [PATCH 17/22] Commenting out missing imports --- custom_slack_provider/slack.py | 2 +- hackathon/tasks.py | 5 +++-- teams/views.py | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/custom_slack_provider/slack.py b/custom_slack_provider/slack.py index 0953517b..57ef6922 100644 --- a/custom_slack_provider/slack.py +++ b/custom_slack_provider/slack.py @@ -65,7 +65,7 @@ def leave_channel(self, channel): self.leave_conversation_url, data=data) if not leave_channel.get('ok'): - print(('An error occurred adding users to the Private Slack Channel. ' + print(('An error occurred leaving a Slack Channel. ' f'Error code: {leave_channel.get("error")}')) return leave_channel diff --git a/hackathon/tasks.py b/hackathon/tasks.py index 2b5948db..2f4fbf65 100644 --- a/hackathon/tasks.py +++ b/hackathon/tasks.py @@ -14,7 +14,7 @@ from custom_slack_provider.slack import CustomSlackClient from hackathon.models import Hackathon -from teams.tasks import remove_admin_from_channel +#from teams.tasks import remove_admin_from_channel logger = logging.getLogger(__name__) @@ -101,7 +101,8 @@ def create_new_hackathon_slack_channel(hackathon_id, channel_name): return if slack_site_settings.remove_admin_from_channel: - remove_admin_from_channel(users_to_invite, channel) +# remove_admin_from_channel(users_to_invite, channel) + pass @shared_task diff --git a/teams/views.py b/teams/views.py index 0e6164da..b3156a3c 100644 --- a/teams/views.py +++ b/teams/views.py @@ -23,7 +23,7 @@ create_teams_in_view, update_team_participants, calculate_timezone_offset) from teams.forms import HackProjectForm, EditTeamName -from teams.tasks import remove_admin_from_channel +# from teams.tasks import remove_admin_from_channel SLACK_CHANNEL_ENDPOINT = 'https://slack.com/api/conversations.create' SLACK_CHANNEL_INVITE_ENDPOINT = 'https://slack.com/api/conversations.invite' @@ -313,7 +313,8 @@ def create_private_channel(request, team_id): messages.success(request, 'Private Slack Channel successfully created') if slack_site_settings.remove_admin_from_channel: - remove_admin_from_channel.apply_async(args=[users_to_invite, channel]) +# remove_admin_from_channel.apply_async(args=[users_to_invite, channel]) + pass return redirect(reverse('view_team', kwargs={'team_id': team_id})) From a6d361382346e318ab4e36bcbaa2ef6208f7494d Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Fri, 27 Sep 2024 16:40:03 +0100 Subject: [PATCH 18/22] Rebuilding migrations after rebasing from master --- .../migrations/0051_auto_20240911_1306.py | 18 --------------- ...123_1219.py => 0051_auto_20240927_1539.py} | 18 +++++++++++++-- .../migrations/0052_auto_20240912_1324.py | 18 --------------- .../migrations/0053_auto_20240912_1527.py | 22 ------------------- 4 files changed, 16 insertions(+), 60 deletions(-) delete mode 100644 hackathon/migrations/0051_auto_20240911_1306.py rename hackathon/migrations/{0049_auto_20230123_1219.py => 0051_auto_20240927_1539.py} (61%) delete mode 100644 hackathon/migrations/0052_auto_20240912_1324.py delete mode 100644 hackathon/migrations/0053_auto_20240912_1527.py diff --git a/hackathon/migrations/0051_auto_20240911_1306.py b/hackathon/migrations/0051_auto_20240911_1306.py deleted file mode 100644 index 3387fc24..00000000 --- a/hackathon/migrations/0051_auto_20240911_1306.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1.13 on 2024-09-11 13:06 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('hackathon', '0050_hackathon_google_registrations_form'), - ] - - operations = [ - migrations.RenameField( - model_name='hackathon', - old_name='google_registrations_form', - new_name='google_registration_form', - ), - ] diff --git a/hackathon/migrations/0049_auto_20230123_1219.py b/hackathon/migrations/0051_auto_20240927_1539.py similarity index 61% rename from hackathon/migrations/0049_auto_20230123_1219.py rename to hackathon/migrations/0051_auto_20240927_1539.py index c71bc8a0..e24e8210 100644 --- a/hackathon/migrations/0049_auto_20230123_1219.py +++ b/hackathon/migrations/0051_auto_20240927_1539.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1.13 on 2023-01-23 12:19 +# Generated by Django 3.1.13 on 2024-09-27 15:39 from django.conf import settings from django.db import migrations, models @@ -8,10 +8,24 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('hackathon', '0048_auto_20221219_1655'), + ('hackathon', '0050_hackathon_google_registrations_form'), ] operations = [ + migrations.RenameField( + model_name='hackathon', + old_name='google_registrations_form', + new_name='registration_form', + ), + migrations.RemoveField( + model_name='hackathon', + name='is_register', + ), + migrations.AddField( + model_name='hackathon', + name='allow_external_registrations', + field=models.BooleanField(default=False), + ), migrations.AddField( model_name='hackathon', name='channel_admins', diff --git a/hackathon/migrations/0052_auto_20240912_1324.py b/hackathon/migrations/0052_auto_20240912_1324.py deleted file mode 100644 index 221661a3..00000000 --- a/hackathon/migrations/0052_auto_20240912_1324.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1.13 on 2024-09-12 13:24 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('hackathon', '0051_auto_20240911_1306'), - ] - - operations = [ - migrations.RenameField( - model_name='hackathon', - old_name='google_registration_form', - new_name='registration_form', - ), - ] diff --git a/hackathon/migrations/0053_auto_20240912_1527.py b/hackathon/migrations/0053_auto_20240912_1527.py deleted file mode 100644 index c6b74a02..00000000 --- a/hackathon/migrations/0053_auto_20240912_1527.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 3.1.13 on 2024-09-12 15:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('hackathon', '0052_auto_20240912_1324'), - ] - - operations = [ - migrations.RemoveField( - model_name='hackathon', - name='is_register', - ), - migrations.AddField( - model_name='hackathon', - name='allow_external_registrations', - field=models.BooleanField(default=False), - ), - ] From 927a6259a26c676ba78afda2043d247ff88a656d Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Tue, 8 Oct 2024 14:35:32 +0100 Subject: [PATCH 19/22] Update where admins come from --- hackathon/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hackathon/tasks.py b/hackathon/tasks.py index 2f4fbf65..0d82852a 100644 --- a/hackathon/tasks.py +++ b/hackathon/tasks.py @@ -77,7 +77,7 @@ def create_new_hackathon_slack_channel(hackathon_id, channel_name): logger.info(f"Channel with id {channel} created.") # Add admins to channel for administration purposes - users = [admin.username for admin in slack_site_settings.slack_admins.all()] + users = [admin.username for admin in hackathon.channel_admins.all()] # First need to add Slack Bot to then add users to channel response = admin_client.invite_users_to_slack_channel( users=settings.SLACK_BOT_ID, From 5033a7a4e660e25a4c405abf1572125b88e33f7e Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Fri, 11 Oct 2024 17:04:09 +0100 Subject: [PATCH 20/22] Adding tasks to create general Slack channel and automate invite and kick users --- accounts/models.py | 4 +- custom_slack_provider/slack.py | 12 ++- custom_slack_provider/tests.py | 4 +- hackathon/forms.py | 5 +- .../migrations/0051_auto_20240911_1306.py | 18 +++++ .../migrations/0052_auto_20240912_1324.py | 18 +++++ .../migrations/0053_auto_20240912_1527.py | 22 +++++ ...927_1539.py => 0056_auto_20241011_1357.py} | 18 +---- hackathon/tasks.py | 81 ++++++++++++------- hackathon/tests/task_tests.py | 57 ++++++++++--- hackathon/views.py | 14 ++-- runtests.sh | 5 ++ 12 files changed, 185 insertions(+), 73 deletions(-) create mode 100644 hackathon/migrations/0051_auto_20240911_1306.py create mode 100644 hackathon/migrations/0052_auto_20240912_1324.py create mode 100644 hackathon/migrations/0053_auto_20240912_1527.py rename hackathon/migrations/{0051_auto_20240927_1539.py => 0056_auto_20241011_1357.py} (61%) create mode 100755 runtests.sh diff --git a/accounts/models.py b/accounts/models.py index 0ffc433f..8c8be263 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -193,7 +193,9 @@ def user_type(self): else: # A non-specified group return None - + @property + def is_admin(self): + return self.user_type in [UserType.SUPERUSER, UserType.STAFF, UserType.PARTNER_ADMIN, UserType.FACILITATOR_ADMIN] class SlackSiteSettings(SingletonModel): """ Model to set how the showcase should be constructed""" diff --git a/custom_slack_provider/slack.py b/custom_slack_provider/slack.py index 57ef6922..ccaa37c1 100644 --- a/custom_slack_provider/slack.py +++ b/custom_slack_provider/slack.py @@ -28,6 +28,7 @@ class CustomSlackClient(): user_detail_url = 'https://slack.com/api/users.info' create_conversation_url = 'https://slack.com/api/conversations.create' invite_conversation_url = 'https://slack.com/api/conversations.invite' + kick_conversation_url = 'https://slack.com/api/conversations.kick' leave_conversation_url = 'https://slack.com/api/conversations.leave' def __init__(self, token): @@ -55,7 +56,10 @@ def _make_slack_post_request(self, url, data): return resp.json() def get_identity(self): - return self._make_slack_get_request(self.identity_url) + identity = self._make_slack_get_request(self.identity_url) + if not identity.get('ok'): + raise SlackException(identity.get("error")) + return identity def leave_channel(self, channel): data = { @@ -65,7 +69,7 @@ def leave_channel(self, channel): self.leave_conversation_url, data=data) if not leave_channel.get('ok'): - print(('An error occurred leaving a Slack Channel. ' + logger.error(('An error occurred leaving a Slack Channel. ' f'Error code: {leave_channel.get("error")}')) return leave_channel @@ -107,7 +111,7 @@ def _extract_userid_from_username(self, username): when Slack is enabled and the account was created with a valid userid schema: [SLACK_USER_ID]_[WORKSPACE_TEAM_ID]""" if not re.match(r'[A-Z0-9]*[_]T[A-Z0-9]*', username): - raise SlackException('Error adding user to channel') + raise SlackException('Error adding user %s to channel' % username) return username.split('_')[0] def invite_users_to_slack_channel(self, users, channel): @@ -133,7 +137,7 @@ def kick_user_from_slack_channel(self, user, channel): "channel": channel, } user_added = self._make_slack_post_request( - self.invite_conversation_url, data=data) + self.kick_conversation_url, data=data) if not user_added.get('ok'): return { diff --git a/custom_slack_provider/tests.py b/custom_slack_provider/tests.py index c1c1541f..458cf3cd 100644 --- a/custom_slack_provider/tests.py +++ b/custom_slack_provider/tests.py @@ -69,7 +69,7 @@ def test__make_slack_post_request(self, post): @patch('custom_slack_provider.slack.CustomSlackClient._make_slack_get_request') # noqa: 501 def test_get_identity(self, _make_slack_get_request): - _make_slack_get_request.return_value = {'user': {'id': 1}} + _make_slack_get_request.return_value = {'user': {'id': 1}, 'ok': True} client = CustomSlackClient(self.token) response = client.get_identity() self.assertEqual(response['user']['id'], 1) @@ -84,7 +84,7 @@ def test__extract_userid_from_username(self): userid = client._extract_userid_from_username(invalid_username) except SlackException as e: self.assertTrue(isinstance(e, SlackException)) - self.assertEquals(e.message, 'Error adding user to channel') + self.assertEquals(e.message, 'Error adding user bob@bob.com to channel') @patch('custom_slack_provider.slack.CustomSlackClient._make_slack_post_request') # noqa: 501 def test_invite_users_to_slack_channel(self, _make_slack_post_request): diff --git a/hackathon/forms.py b/hackathon/forms.py index 55ba18a9..25ef4727 100644 --- a/hackathon/forms.py +++ b/hackathon/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.db.models import Q from easy_select2 import Select2Multiple from accounts.models import Organisation @@ -125,7 +126,7 @@ class HackathonForm(forms.ModelForm): channel_admins = forms.ModelMultipleChoiceField( label="Channel Admins", required=False, - queryset=User.objects.all(), + queryset=User.objects.filter(Q(is_superuser=True) | Q(is_staff=True)), widget=Select2Multiple(select2attrs={'width': '100%'}) ) @@ -286,4 +287,4 @@ def save(self, commit=True): event.body += f'

Meeting Join Link: Click here to join
Meeting Join Code: {webinar_code}' if commit: event.save() - return event \ No newline at end of file + return event diff --git a/hackathon/migrations/0051_auto_20240911_1306.py b/hackathon/migrations/0051_auto_20240911_1306.py new file mode 100644 index 00000000..3387fc24 --- /dev/null +++ b/hackathon/migrations/0051_auto_20240911_1306.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.13 on 2024-09-11 13:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('hackathon', '0050_hackathon_google_registrations_form'), + ] + + operations = [ + migrations.RenameField( + model_name='hackathon', + old_name='google_registrations_form', + new_name='google_registration_form', + ), + ] diff --git a/hackathon/migrations/0052_auto_20240912_1324.py b/hackathon/migrations/0052_auto_20240912_1324.py new file mode 100644 index 00000000..221661a3 --- /dev/null +++ b/hackathon/migrations/0052_auto_20240912_1324.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.13 on 2024-09-12 13:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('hackathon', '0051_auto_20240911_1306'), + ] + + operations = [ + migrations.RenameField( + model_name='hackathon', + old_name='google_registration_form', + new_name='registration_form', + ), + ] diff --git a/hackathon/migrations/0053_auto_20240912_1527.py b/hackathon/migrations/0053_auto_20240912_1527.py new file mode 100644 index 00000000..c6b74a02 --- /dev/null +++ b/hackathon/migrations/0053_auto_20240912_1527.py @@ -0,0 +1,22 @@ +# Generated by Django 3.1.13 on 2024-09-12 15:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hackathon', '0052_auto_20240912_1324'), + ] + + operations = [ + migrations.RemoveField( + model_name='hackathon', + name='is_register', + ), + migrations.AddField( + model_name='hackathon', + name='allow_external_registrations', + field=models.BooleanField(default=False), + ), + ] diff --git a/hackathon/migrations/0051_auto_20240927_1539.py b/hackathon/migrations/0056_auto_20241011_1357.py similarity index 61% rename from hackathon/migrations/0051_auto_20240927_1539.py rename to hackathon/migrations/0056_auto_20241011_1357.py index e24e8210..93974daf 100644 --- a/hackathon/migrations/0051_auto_20240927_1539.py +++ b/hackathon/migrations/0056_auto_20241011_1357.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1.13 on 2024-09-27 15:39 +# Generated by Django 3.1.13 on 2024-10-11 13:57 from django.conf import settings from django.db import migrations, models @@ -8,24 +8,10 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('hackathon', '0050_hackathon_google_registrations_form'), + ('hackathon', '0055_remove_event_isreadonly'), ] operations = [ - migrations.RenameField( - model_name='hackathon', - old_name='google_registrations_form', - new_name='registration_form', - ), - migrations.RemoveField( - model_name='hackathon', - name='is_register', - ), - migrations.AddField( - model_name='hackathon', - name='allow_external_registrations', - field=models.BooleanField(default=False), - ), migrations.AddField( model_name='hackathon', name='channel_admins', diff --git a/hackathon/tasks.py b/hackathon/tasks.py index 0d82852a..5434240b 100644 --- a/hackathon/tasks.py +++ b/hackathon/tasks.py @@ -7,6 +7,7 @@ from django.core.mail import send_mail from smtplib import SMTPException +from accounts.models import CustomUser as User from accounts.models import EmailTemplate, SlackSiteSettings from celery import shared_task @@ -60,6 +61,10 @@ def create_new_hackathon_slack_channel(hackathon_id, channel_name): f"{hackathon.display_name} in Slack Workspace " f"{settings.SLACK_WORKSPACE}({settings.SLACK_TEAM_ID})")) + # Use a workspace admin's User Token to create the channel + # This is required because a bot is seen as a Slack member and + # if a workspace is set to only allow admins to create channels + # creating the channel with a Bot Token will give an error admin_client = CustomSlackClient(settings.SLACK_ADMIN_TOKEN) channel_response = admin_client.create_slack_channel( team_id=settings.SLACK_TEAM_ID, @@ -67,49 +72,65 @@ def create_new_hackathon_slack_channel(hackathon_id, channel_name): is_private=True, ) - if not channel_response['ok']: - logger.error(channel_response['error']) - channel = channel_response.get('channel', {}).get('id') channel_url = f'https://{settings.SLACK_WORKSPACE}.slack.com/archives/{channel}' hackathon.channel_url = channel_url hackathon.save() logger.info(f"Channel with id {channel} created.") - + # Add admins to channel for administration purposes - users = [admin.username for admin in hackathon.channel_admins.all()] - # First need to add Slack Bot to then add users to channel - response = admin_client.invite_users_to_slack_channel( - users=settings.SLACK_BOT_ID, - channel=channel, - ) - if not response['ok']: - logger.error(response['error']) - return - - bot_client = CustomSlackClient(settings.SLACK_BOT_TOKEN) + admin_usernames = [admin.username for admin in hackathon.channel_admins.all()] pattern = re.compile(r'^U[a-zA-Z0-9]*[_]T[a-zA-Z0-9]*$') - users_to_invite = ','.join([user.split('_')[0] - for user in users if pattern.match(user)]) - bot_client.invite_users_to_slack_channel( - users=users_to_invite, + admin_user_ids = ','.join([username.split('_')[0] + for username in admin_usernames + if pattern.match(username)]) + admin_client.invite_users_to_slack_channel( + users=admin_user_ids, channel=channel, ) - if not response['ok']: - logger.error(response['error']) - return - - if slack_site_settings.remove_admin_from_channel: -# remove_admin_from_channel(users_to_invite, channel) - pass - @shared_task def invite_user_to_hackathon_slack_channel(hackathon_id, user_id): - bot_client = CustomSlackClient(settings.SLACK_BOT_TOKEN) + slack_site_settings = SlackSiteSettings.objects.first() + if (not (settings.SLACK_ENABLED or settings.SLACK_BOT_TOKEN or settings.SLACK_ADMIN_TOKEN + or settings.SLACK_WORKSPACE or not slack_site_settings)): + logger.info("This feature is not enabeled.") + return + + hackathon = Hackathon.objects.get(id=hackathon_id) + user = User.objects.get(id=user_id) + logger.info(f"Inviting user {user_id} to hackathon {hackathon_id}'s slack channel") + + admin_client = CustomSlackClient(settings.SLACK_ADMIN_TOKEN) + channel = hackathon.channel_url.split('/')[-1] + pattern = re.compile(r'^U[a-zA-Z0-9]*[_]T[a-zA-Z0-9]*$') + slack_user_id = admin_client._extract_userid_from_username(user.username) + admin_client.invite_users_to_slack_channel( + users=[slack_user_id], + channel=channel + ) + logger.info(f"Successfully invited user {user_id} to hackathon {hackathon_id}'s slack channel") @shared_task -def kick_user_to_hackathon_slack_channel(user, channel): - pass +def kick_user_from_hackathon_slack_channel(hackathon_id, user_id): + slack_site_settings = SlackSiteSettings.objects.first() + if (not (settings.SLACK_ENABLED or settings.SLACK_BOT_TOKEN or settings.SLACK_ADMIN_TOKEN + or settings.SLACK_WORKSPACE or not slack_site_settings)): + logger.info("This feature is not enabeled.") + return + + hackathon = Hackathon.objects.get(id=hackathon_id) + user = User.objects.get(id=user_id) + logger.info(f"Kicking user {user_id} to hackathon {hackathon_id}'s slack channel") + admin_client = CustomSlackClient(settings.SLACK_ADMIN_TOKEN) + channel = hackathon.channel_url.split('/')[-1] + pattern = re.compile(r'^U[a-zA-Z0-9]*[_]T[a-zA-Z0-9]*$') + slack_user_id = admin_client._extract_userid_from_username(user.username) + kicked = admin_client.kick_user_from_slack_channel( + user=slack_user_id, + channel=channel + ) + logger.info(f"Successfully kicked user {user_id} to hackathon {hackathon_id}'s slack channel") + diff --git a/hackathon/tests/task_tests.py b/hackathon/tests/task_tests.py index f667191c..cd3db6d9 100644 --- a/hackathon/tests/task_tests.py +++ b/hackathon/tests/task_tests.py @@ -6,8 +6,11 @@ from unittest.mock import patch, Mock from accounts.models import Organisation, CustomUser as User +from accounts.models import SlackSiteSettings from hackathon.models import Hackathon -from hackathon.tasks import create_new_hackathon_slack_channel +from hackathon.tasks import create_new_hackathon_slack_channel, \ + invite_user_to_hackathon_slack_channel, \ + kick_user_from_hackathon_slack_channel class TaskTests(TestCase): @@ -18,6 +21,9 @@ def setUp(self): slack_display_name="bob", organisation=organisation, ) + self.slack_site_settings = SlackSiteSettings.objects.create( + remove_admin_from_channel=True + ) self.hackathon = Hackathon.objects.create( created_by=self.user, display_name="hacktest", @@ -25,10 +31,8 @@ def setUp(self): start_date=f'{datetime.now()}', end_date=f'{datetime.now()}') self.hackathon.channel_admins.add(self.user) - - @override_settings(CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, - CELERY_ALWAYS_EAGER=True, - BROKER_BACKEND='memory') + + @responses.activate def test_create_new_hackathon_slack_channel(self): channel_id = 'CH123123' responses.add( @@ -37,10 +41,45 @@ def test_create_new_hackathon_slack_channel(self): responses.add( responses.POST, 'https://slack.com/api/conversations.invite', json={'ok': True}, status=200) - create_new_hackathon_slack_channel.apply_async(args=[ - self.hackathon.id, self.user.username]) + responses.add( + responses.POST, 'https://slack.com/api/users.identity', + json={'ok': True}, status=200) + responses.add( + responses.POST, 'https://slack.com/api/conversations.leave', + json={'ok': True}, status=200) - import time; time.sleep(3) + create_new_hackathon_slack_channel(self.hackathon.id, self.user.username) + self.assertEquals( - self.hackathon.channel_url, + Hackathon.objects.first().channel_url, f'https://{settings.SLACK_WORKSPACE}.slack.com/archives/{channel_id}') + + @responses.activate + def test_invite_user_to_channel(self): + channel_id = 'CH123123' + self.hackathon.channel_url = 'https://{settings.SLACK_WORKSPACE}.slack.com/archives/{channel_id}' + self.hackathon.save() + + responses.add( + responses.POST, 'https://slack.com/api/conversations.invite', + json={'ok': True}, status=200) + + try: + invite_user_to_hackathon_slack_channel(self.hackathon.id, self.user.id) + except: + raise Exception("Inviting user to channel failed") + + @responses.activate + def test_kick_user_from_channel(self): + channel_id = 'CH123123' + self.hackathon.channel_url = 'https://{settings.SLACK_WORKSPACE}.slack.com/archives/{channel_id}' + self.hackathon.save() + + responses.add( + responses.POST, 'https://slack.com/api/conversations.kick', + json={'ok': True}, status=200) + + try: + kick_user_from_hackathon_slack_channel(self.hackathon.id, self.user.id) + except: + raise Exception("Kicking user from channel failed") diff --git a/hackathon/views.py b/hackathon/views.py index 79b7f5cb..4437df8d 100644 --- a/hackathon/views.py +++ b/hackathon/views.py @@ -24,11 +24,12 @@ from .lists import AWARD_CATEGORIES from .helpers import format_date, query_scores, create_judges_scores_table from .tasks import send_email_from_template -from .tasks import create_new_hackathon_slack_channel +from .tasks import create_new_hackathon_slack_channel, \ + invite_user_to_hackathon_slack_channel, \ + kick_user_from_hackathon_slack_channel from accounts.models import UserType from accounts.decorators import can_access, has_access_to_hackathon -from custom_slack_provider.slack import CustomSlackClient #Calendar for hackathon import calendar @@ -467,7 +468,6 @@ def delete_hackathon(request, hackathon_id): @has_access_to_hackathon() def enroll_toggle(request): if request.method == "POST": - bot_client = CustomSlackClient(settings.SLACK_BOT_TOKEN) judge_user_types = [ UserType.SUPERUSER, UserType.STAFF, UserType.FACILITATOR_ADMIN, UserType.FACILITATOR_JUDGE, UserType.PARTNER_ADMIN, @@ -482,9 +482,7 @@ def enroll_toggle(request): elif request.user in hackathon.participants.all(): hackathon.participants.remove(request.user) if hackathon.channel_url: - channel = hackathon.channel_url.split('/')[-1] - slack_user_id = request.user.username.split('_')[0] - bot_client.kick_user_from_slack_channel(slack_user_id, channel) + kick_user_from_hackathon_slack_channel.apply_async(args=[hackathon.id, request.user.id]) send_email_from_template.apply_async(args=[request.user.email, request.user.first_name, hackathon.display_name, 'withdraw_participant']) messages.success(request, "You have withdrawn from this Hackaton.") @@ -501,9 +499,7 @@ def enroll_toggle(request): 'hackathon_id': request.POST.get("hackathon-id")})) hackathon.participants.add(request.user) if hackathon.channel_url: - channel = hackathon.channel_url.split('/')[-1] - slack_user_id = request.user.username.split('_')[0] - bot_client.invite_users_to_slack_channel(slack_user_id, channel) + invite_user_to_hackathon_slack_channel.apply_async(args=[hackathon.id, request.user.id]) send_email_from_template.apply_async(args=[request.user.email, request.user.first_name, hackathon.display_name, 'enroll_participant']) messages.success(request, "You have enrolled successfully.") diff --git a/runtests.sh b/runtests.sh new file mode 100755 index 00000000..796f0042 --- /dev/null +++ b/runtests.sh @@ -0,0 +1,5 @@ +#! /bin/bash + +set -e + +docker compose exec hackathon-app python3 manage.py test $1 From e3541c0a40c9d5d867c94c7642741db61188e68f Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Mon, 14 Oct 2024 10:21:47 +0100 Subject: [PATCH 21/22] Adding missing leave functionality on team channel creation page --- ...sitesettings_use_hackathon_slack_admins.py | 18 +++++++++++++++++ accounts/models.py | 5 +++++ hackathon/tasks.py | 5 ++++- teams/views.py | 20 ++++++++++++------- 4 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 accounts/migrations/0022_slacksitesettings_use_hackathon_slack_admins.py diff --git a/accounts/migrations/0022_slacksitesettings_use_hackathon_slack_admins.py b/accounts/migrations/0022_slacksitesettings_use_hackathon_slack_admins.py new file mode 100644 index 00000000..5ba81fc0 --- /dev/null +++ b/accounts/migrations/0022_slacksitesettings_use_hackathon_slack_admins.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.13 on 2024-10-11 16:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0021_slacksitesettings_remove_admin_from_channel'), + ] + + operations = [ + migrations.AddField( + model_name='slacksitesettings', + name='use_hackathon_slack_admins', + field=models.BooleanField(default=False), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 8c8be263..145a2b88 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -211,6 +211,11 @@ class SlackSiteSettings(SingletonModel): "be added to any new channels. If this is ticked, the user " "will be removed from private team channels if they are not " "part of the team, facilitator or the slack admins")) + use_hackathon_slack_admins = models.BooleanField( + default=False, + help_text=("If ticked, the global Slack Admins will be ignored " + "and the slack admins who are selected when creating " + "the hackathon will be used instead")) def __str__(self): return "Slack Settings" diff --git a/hackathon/tasks.py b/hackathon/tasks.py index 5434240b..d92b5155 100644 --- a/hackathon/tasks.py +++ b/hackathon/tasks.py @@ -79,7 +79,10 @@ def create_new_hackathon_slack_channel(hackathon_id, channel_name): logger.info(f"Channel with id {channel} created.") # Add admins to channel for administration purposes - admin_usernames = [admin.username for admin in hackathon.channel_admins.all()] + slack_admins = (hackathon.channel_admins.all() + if slack_site_settings.use_hackathon_slack_admins + else slack_site_settings.slack_admins.all()) + admin_usernames = [admin.username for admin in slack_admins] pattern = re.compile(r'^U[a-zA-Z0-9]*[_]T[a-zA-Z0-9]*$') admin_user_ids = ','.join([username.split('_')[0] for username in admin_usernames diff --git a/teams/views.py b/teams/views.py index b3156a3c..3bdcc39a 100644 --- a/teams/views.py +++ b/teams/views.py @@ -23,7 +23,6 @@ create_teams_in_view, update_team_participants, calculate_timezone_offset) from teams.forms import HackProjectForm, EditTeamName -# from teams.tasks import remove_admin_from_channel SLACK_CHANNEL_ENDPOINT = 'https://slack.com/api/conversations.create' SLACK_CHANNEL_INVITE_ENDPOINT = 'https://slack.com/api/conversations.invite' @@ -47,8 +46,13 @@ def change_teams(request, hackathon_id): team_size = hackathon.team_size team_sizes = sorted(choose_team_sizes(participants, team_size)) if len(team_sizes) == 0: - return render(request, 'change_teams.html', - {'num_participants': len(participants)}) + return render(request, 'change_teams.html', { + 'num_participants': len(participants), + 'hackathon_id': hackathon_id, + 'teams': [], + 'leftover_participants': [], + 'edit': edit, + }) grouped_participants, hackathon_level = group_participants( participants, len(team_sizes)) team_levels = sorted(choose_team_levels(len(team_sizes), hackathon_level)) @@ -279,7 +283,10 @@ def create_private_channel(request, team_id): users.append(team.mentor.username) # Add admins to channel for administration purposes - for admin in slack_site_settings.slack_admins.all(): + slack_admins = (team.hackathon.channel_admins.all() + if slack_site_settings.use_hackathon_slack_admins + else slack_site_settings.slack_admins.all()) + for admin in slack_admins: users.append(admin.username) # First need to add Slack Bot to then add users to channel response = admin_client.invite_users_to_slack_channel( @@ -311,10 +318,9 @@ def create_private_channel(request, team_id): f'Please add the missing users manually.')) else: messages.success(request, 'Private Slack Channel successfully created') - + if slack_site_settings.remove_admin_from_channel: -# remove_admin_from_channel.apply_async(args=[users_to_invite, channel]) - pass + admin_client.leave_channel(channel) return redirect(reverse('view_team', kwargs={'team_id': team_id})) From 436c6e2c395bd4c5758a942fdb51bf45b903b34f Mon Sep 17 00:00:00 2001 From: Stefan Dworschak Date: Mon, 14 Oct 2024 10:28:52 +0100 Subject: [PATCH 22/22] Removing unneeded code --- hackathon/tasks.py | 1 - main/settings.py | 8 +------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/hackathon/tasks.py b/hackathon/tasks.py index d92b5155..3b14d6d2 100644 --- a/hackathon/tasks.py +++ b/hackathon/tasks.py @@ -15,7 +15,6 @@ from custom_slack_provider.slack import CustomSlackClient from hackathon.models import Hackathon -#from teams.tasks import remove_admin_from_channel logger = logging.getLogger(__name__) diff --git a/main/settings.py b/main/settings.py index 7286a40e..a246a898 100644 --- a/main/settings.py +++ b/main/settings.py @@ -167,6 +167,7 @@ ] # Celery +CELERY_IMPORTS = ("hackathon.tasks", ) CELERY_BROKER_URL = os.environ.get('CELERY_BROKER', 'redis://redis:6379') CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://redis:6379') # noqa: E501 CELERY_ACCEPT_CONTENT = os.environ.get('CELERY_ACCEPT_CONTENT', 'application/json').split(',') # noqa: E501 @@ -225,10 +226,3 @@ integrations=[DjangoIntegration()] ) - -CELERY_IMPORTS = ("hackathon.tasks", ) -CELERY_BROKER_URL = os.environ.get('CELERY_BROKER', 'redis://redis:6379') -CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://redis:6379') # noqa: E501 -CELERY_ACCEPT_CONTENT = os.environ.get('CELERY_ACCEPT_CONTENT', 'application/json').split(',') # noqa: E501 -CELERY_TASK_SERIALIZER = os.environ.get('CELERY_TASK_SERIALIZER', 'json') -CELERY_RESULT_SERIALIZER = os.environ.get('CELERY_RESULT_SERIALIZER', 'json')