From 0dda57c038219a7570ce9e9efa2bbc93e890965c Mon Sep 17 00:00:00 2001 From: Steve Brownlee Date: Thu, 29 Aug 2024 16:03:20 -0400 Subject: [PATCH 1/9] Teams, student assignment, and team/project assignment done Also updated project view to store client/server template URLs --- ...team_nssuserteam_groupprojectrepository.py | 93 +++++++++++++++++++ .../0053_alter_studentteam_group_name.py | 17 ++++ ...userteam_student_alter_nssuserteam_team.py | 31 +++++++ ...bers_alter_nssuserteam_student_and_more.py | 35 +++++++ ...056_rename_members_studentteam_students.py | 17 ++++ .../migrations/0057_project_template_url.py | 17 ++++ ...e_url_project_api_template_url_and_more.py | 26 ++++++ ...e_groupprojectrepository_repository_url.py | 19 ++++ LearningAPI/models/coursework/project.py | 5 +- LearningAPI/models/people/__init__.py | 5 +- .../models/people/group_project_repo.py | 19 ++++ LearningAPI/models/people/nssuser_team.py | 7 ++ LearningAPI/models/people/student_team.py | 10 ++ LearningAPI/views/__init__.py | 3 +- LearningAPI/views/project_view.py | 3 + LearningAPI/views/team_maker_view.py | 53 +++++++++++ LearningPlatform/urls.py | 1 + learningplatform.dbml | 21 +++++ learopsdev.session.sql | 7 +- 19 files changed, 381 insertions(+), 8 deletions(-) create mode 100644 LearningAPI/migrations/0052_studentteam_nssuserteam_groupprojectrepository.py create mode 100644 LearningAPI/migrations/0053_alter_studentteam_group_name.py create mode 100644 LearningAPI/migrations/0054_alter_nssuserteam_student_alter_nssuserteam_team.py create mode 100644 LearningAPI/migrations/0055_studentteam_members_alter_nssuserteam_student_and_more.py create mode 100644 LearningAPI/migrations/0056_rename_members_studentteam_students.py create mode 100644 LearningAPI/migrations/0057_project_template_url.py create mode 100644 LearningAPI/migrations/0058_remove_project_template_url_project_api_template_url_and_more.py create mode 100644 LearningAPI/migrations/0059_remove_groupprojectrepository_repository_url.py create mode 100644 LearningAPI/models/people/group_project_repo.py create mode 100644 LearningAPI/models/people/nssuser_team.py create mode 100644 LearningAPI/models/people/student_team.py create mode 100644 LearningAPI/views/team_maker_view.py diff --git a/LearningAPI/migrations/0052_studentteam_nssuserteam_groupprojectrepository.py b/LearningAPI/migrations/0052_studentteam_nssuserteam_groupprojectrepository.py new file mode 100644 index 0000000..d76bab9 --- /dev/null +++ b/LearningAPI/migrations/0052_studentteam_nssuserteam_groupprojectrepository.py @@ -0,0 +1,93 @@ +# Generated by Django 4.2.14 on 2024-08-28 20:03 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("LearningAPI", "0051_add_student_note_type_to_database_function"), + ] + + operations = [ + migrations.CreateModel( + name="StudentTeam", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("group_name", models.CharField(max_length=55, unique=True)), + ("sprint_team", models.BooleanField(default=False)), + ( + "cohort", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="LearningAPI.cohort", + ), + ), + ], + ), + migrations.CreateModel( + name="NSSUserTeam", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "student", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="LearningAPI.nssuser", + ), + ), + ( + "team", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="LearningAPI.studentteam", + ), + ), + ], + ), + migrations.CreateModel( + name="GroupProjectRepository", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("repository_url", models.URLField()), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="LearningAPI.project", + ), + ), + ( + "team", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="LearningAPI.studentteam", + ), + ), + ], + ), + ] diff --git a/LearningAPI/migrations/0053_alter_studentteam_group_name.py b/LearningAPI/migrations/0053_alter_studentteam_group_name.py new file mode 100644 index 0000000..51fc346 --- /dev/null +++ b/LearningAPI/migrations/0053_alter_studentteam_group_name.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.14 on 2024-08-28 20:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("LearningAPI", "0052_studentteam_nssuserteam_groupprojectrepository"), + ] + + operations = [ + migrations.AlterField( + model_name="studentteam", + name="group_name", + field=models.CharField(max_length=55), + ), + ] diff --git a/LearningAPI/migrations/0054_alter_nssuserteam_student_alter_nssuserteam_team.py b/LearningAPI/migrations/0054_alter_nssuserteam_student_alter_nssuserteam_team.py new file mode 100644 index 0000000..404cfc2 --- /dev/null +++ b/LearningAPI/migrations/0054_alter_nssuserteam_student_alter_nssuserteam_team.py @@ -0,0 +1,31 @@ +# Generated by Django 4.2.14 on 2024-08-28 20:47 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("LearningAPI", "0053_alter_studentteam_group_name"), + ] + + operations = [ + migrations.AlterField( + model_name="nssuserteam", + name="student", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="teams", + to="LearningAPI.nssuser", + ), + ), + migrations.AlterField( + model_name="nssuserteam", + name="team", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="members", + to="LearningAPI.studentteam", + ), + ), + ] diff --git a/LearningAPI/migrations/0055_studentteam_members_alter_nssuserteam_student_and_more.py b/LearningAPI/migrations/0055_studentteam_members_alter_nssuserteam_student_and_more.py new file mode 100644 index 0000000..64a2ce3 --- /dev/null +++ b/LearningAPI/migrations/0055_studentteam_members_alter_nssuserteam_student_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.14 on 2024-08-28 20:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("LearningAPI", "0054_alter_nssuserteam_student_alter_nssuserteam_team"), + ] + + operations = [ + migrations.AddField( + model_name="studentteam", + name="members", + field=models.ManyToManyField( + through="LearningAPI.NSSUserTeam", to="LearningAPI.nssuser" + ), + ), + migrations.AlterField( + model_name="nssuserteam", + name="student", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="LearningAPI.nssuser" + ), + ), + migrations.AlterField( + model_name="nssuserteam", + name="team", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="LearningAPI.studentteam", + ), + ), + ] diff --git a/LearningAPI/migrations/0056_rename_members_studentteam_students.py b/LearningAPI/migrations/0056_rename_members_studentteam_students.py new file mode 100644 index 0000000..f47d758 --- /dev/null +++ b/LearningAPI/migrations/0056_rename_members_studentteam_students.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.14 on 2024-08-28 20:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("LearningAPI", "0055_studentteam_members_alter_nssuserteam_student_and_more"), + ] + + operations = [ + migrations.RenameField( + model_name="studentteam", + old_name="members", + new_name="students", + ), + ] diff --git a/LearningAPI/migrations/0057_project_template_url.py b/LearningAPI/migrations/0057_project_template_url.py new file mode 100644 index 0000000..a61cf16 --- /dev/null +++ b/LearningAPI/migrations/0057_project_template_url.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.14 on 2024-08-29 19:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("LearningAPI", "0056_rename_members_studentteam_students"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="template_url", + field=models.CharField(default="", max_length=256), + ), + ] diff --git a/LearningAPI/migrations/0058_remove_project_template_url_project_api_template_url_and_more.py b/LearningAPI/migrations/0058_remove_project_template_url_project_api_template_url_and_more.py new file mode 100644 index 0000000..1c5ca90 --- /dev/null +++ b/LearningAPI/migrations/0058_remove_project_template_url_project_api_template_url_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.14 on 2024-08-29 19:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("LearningAPI", "0057_project_template_url"), + ] + + operations = [ + migrations.RemoveField( + model_name="project", + name="template_url", + ), + migrations.AddField( + model_name="project", + name="api_template_url", + field=models.URLField(default=""), + ), + migrations.AddField( + model_name="project", + name="client_template_url", + field=models.URLField(default=""), + ), + ] diff --git a/LearningAPI/migrations/0059_remove_groupprojectrepository_repository_url.py b/LearningAPI/migrations/0059_remove_groupprojectrepository_repository_url.py new file mode 100644 index 0000000..7af82c1 --- /dev/null +++ b/LearningAPI/migrations/0059_remove_groupprojectrepository_repository_url.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.14 on 2024-08-29 20:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ( + "LearningAPI", + "0058_remove_project_template_url_project_api_template_url_and_more", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="groupprojectrepository", + name="repository_url", + ), + ] diff --git a/LearningAPI/models/coursework/project.py b/LearningAPI/models/coursework/project.py index 340d073..cc5e047 100644 --- a/LearningAPI/models/coursework/project.py +++ b/LearningAPI/models/coursework/project.py @@ -5,8 +5,9 @@ class Project(models.Model): """Course projects""" name = models.CharField(max_length=55) implementation_url = models.CharField(max_length=256) - book = models.ForeignKey( - "Book", on_delete=models.CASCADE, related_name="child_projects") + client_template_url = models.URLField(default="") + api_template_url = models.URLField(default="") + book = models.ForeignKey("Book", on_delete=models.CASCADE, related_name="child_projects") index = models.IntegerField(default=0) active = models.BooleanField(default=True) is_group_project = models.BooleanField(default=False) diff --git a/LearningAPI/models/people/__init__.py b/LearningAPI/models/people/__init__.py index fc3fce4..df41ead 100644 --- a/LearningAPI/models/people/__init__.py +++ b/LearningAPI/models/people/__init__.py @@ -12,4 +12,7 @@ from .student_note import StudentNote from .student_personality import StudentPersonality from .student_tag import StudentTag -from .student_note_type import StudentNoteType \ No newline at end of file +from .student_note_type import StudentNoteType +from .student_team import StudentTeam +from .nssuser_team import NSSUserTeam +from .group_project_repo import GroupProjectRepository \ No newline at end of file diff --git a/LearningAPI/models/people/group_project_repo.py b/LearningAPI/models/people/group_project_repo.py new file mode 100644 index 0000000..7b48ed7 --- /dev/null +++ b/LearningAPI/models/people/group_project_repo.py @@ -0,0 +1,19 @@ +from django.db import models +from django.core.exceptions import ValidationError + +class GroupProjectRepository(models.Model): + """ This class stores the repository URLs for a group project """ + team = models.ForeignKey("StudentTeam", on_delete=models.CASCADE) + project = models.ForeignKey("Project", on_delete=models.CASCADE) + + def clean(self): + super().clean() + if not self.project.is_group_project and self.project_id.isdigit(): + raise ValidationError({ + 'project': 'Integer IDs are only allowed for projects with is_group_project set to True.' + }) + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + diff --git a/LearningAPI/models/people/nssuser_team.py b/LearningAPI/models/people/nssuser_team.py new file mode 100644 index 0000000..8982cee --- /dev/null +++ b/LearningAPI/models/people/nssuser_team.py @@ -0,0 +1,7 @@ +from django.db import models + +class NSSUserTeam(models.Model): + """ This class is used to store the relationship between a student and a team """ + team = models.ForeignKey("StudentTeam", on_delete=models.CASCADE) + student = models.ForeignKey("NSSUser", on_delete=models.CASCADE) + diff --git a/LearningAPI/models/people/student_team.py b/LearningAPI/models/people/student_team.py new file mode 100644 index 0000000..85b9c4d --- /dev/null +++ b/LearningAPI/models/people/student_team.py @@ -0,0 +1,10 @@ +from django.db import models + +class StudentTeam(models.Model): + """ This class is used to create a student team for a cohort """ + group_name = models.CharField(max_length=55) + cohort = models.ForeignKey("Cohort", on_delete=models.CASCADE) + sprint_team = models.BooleanField(default=False) + students = models.ManyToManyField("NSSUser", through="NSSUserTeam") + + diff --git a/LearningAPI/views/__init__.py b/LearningAPI/views/__init__.py index 2a3c6b5..5be95d0 100644 --- a/LearningAPI/views/__init__.py +++ b/LearningAPI/views/__init__.py @@ -28,4 +28,5 @@ from .student_note_view import StudentNoteViewSet from .personality_view import PersonalityView from .book_assessment import BookAssessmentView -from .student_note_type_view import StudentNoteTypeViewSet \ No newline at end of file +from .student_note_type_view import StudentNoteTypeViewSet +from .team_maker_view import TeamMakerView \ No newline at end of file diff --git a/LearningAPI/views/project_view.py b/LearningAPI/views/project_view.py index 944a849..c81336f 100644 --- a/LearningAPI/views/project_view.py +++ b/LearningAPI/views/project_view.py @@ -22,6 +22,8 @@ def create(self, request): project.index = request.data["index"] project.active = True project.is_group_project = request.data["is_group_project"] + project.api_template_url = request.data["api_template_url"] + project.client_template_url = request.data["client_template_url"] project.book = Book.objects.get(pk=request.data["book"]) project.implementation_url = request.data["implementation_url"] @@ -156,4 +158,5 @@ class Meta: fields = ( 'id', 'name', 'book', 'course', 'index', 'active', 'is_group_project', + 'api_template_url', 'client_template_url', ) diff --git a/LearningAPI/views/team_maker_view.py b/LearningAPI/views/team_maker_view.py new file mode 100644 index 0000000..5a106b1 --- /dev/null +++ b/LearningAPI/views/team_maker_view.py @@ -0,0 +1,53 @@ +import os +import requests + +from rest_framework import serializers, status +from rest_framework.viewsets import ViewSet +from rest_framework.response import Response +from LearningAPI.models.people import StudentTeam, GroupProjectRepository, NSSUserTeam, NssUser + + +class StudentTeamSerializer(serializers.ModelSerializer): + class Meta: + model = StudentTeam + fields = ( 'group_name', 'cohort', 'sprint_team', 'students' ) + + +class TeamMakerView(ViewSet): + """Team Maker""" + def create(self, request): + """Handle POST operations + + Returns: + Response -- JSON serialized instance + """ + cohort_id = request.data.get('cohort', None) + student_list = request.data.get('students', None) + group_project_id = request.data.get('groupProject', None) + + # Create the student team in the database + team = StudentTeam() + team.group_name = "" + team.cohort_id = cohort_id + team.sprint_team = True if group_project_id is not None else False + team.save() + + + # Assign the students to the team + for student in student_list: + student_team = NSSUserTeam() + student_team.student_id = student + student_team.team = team + student_team.save() + + # Create group project repository if group project is not None + if group_project_id is not None: + group_project_repo = GroupProjectRepository() + group_project_repo.team_id = team.id + group_project_repo.project_id = group_project_id + group_project_repo.save() + + serialized_team = StudentTeamSerializer(team, many=False).data + return Response(serialized_team, status=status.HTTP_201_CREATED) + + diff --git a/LearningPlatform/urls.py b/LearningPlatform/urls.py index fe48bd8..10dbad9 100644 --- a/LearningPlatform/urls.py +++ b/LearningPlatform/urls.py @@ -49,6 +49,7 @@ router.register(r'notetypes', views.StudentNoteTypeViewSet, 'notetypes') router.register(r'personalities', views.PersonalityView, 'person') router.register(r'cohortinfo', views.CohortInfoViewSet, 'info') +router.register(r'teams', views.TeamMakerView, 'team_maker') urlpatterns = [ diff --git a/learningplatform.dbml b/learningplatform.dbml index 47f0897..eb5cccd 100644 --- a/learningplatform.dbml +++ b/learningplatform.dbml @@ -209,6 +209,27 @@ Table "LearningAPI_nssusercohort" [headercolor: #c0392b] { } } +Table "LearningAPI_studentteam" [headercolor: #e00ead] { + "id" int [pk, not null, increment] + "cohort_id" int4 [not null, ref: < LearningAPI_cohort.id] + "group_name" varchar + "sprint_team" bool [not null, default: False] + "project_id" int4 [ref: < LearningAPI_project.id] +} + +Table "LearningAPI_groupprojectrepository" [headercolor: #e00ead] { + "id" int [pk, not null, increment] + "team_id" int4 [not null, ref: < LearningAPI_team.id] + "repo_url" varchar [not null] +} + +Table "LearningAPI_nssuserteam" [headercolor: #e00ead] { + "id" int [pk, not null, increment] + "team_id" int4 [not null, ref: < LearningAPI_team.id] + "nss_user_id" int4 [not null, ref: < LearningAPI_nssuser.id] +} + + Table "LearningAPI_objectivetag" [headercolor: #3498db] { "id" int [pk, not null, increment] "tag_id" int4 [not null, ref: < LearningAPI_tag.id] diff --git a/learopsdev.session.sql b/learopsdev.session.sql index ae01d26..be97e15 100644 --- a/learopsdev.session.sql +++ b/learopsdev.session.sql @@ -8,10 +8,9 @@ select * from "socialaccount_socialaccount" order by id desc; select * from "authtoken_token" where user_id = 470; select * from "LearningAPI_nssuser" where user_id = 470; select * from "LearningAPI_nssusercohort" where nss_user_id = 468; -update "LearningAPI_nssusercohort" - set is_github_org_member = FALSE - where nss_user_id = 468; - +DELETE FROM "LearningAPI_nssuserteam"; +DELETE FROM "LearningAPI_groupprojectrepository"; +DELETE FROM "LearningAPI_studentteam"; -- Drop all tables DO $$ DECLARE From 9fb1e199c7ee40ca5aabde30733b51a47691816f Mon Sep 17 00:00:00 2001 From: Steve Brownlee Date: Thu, 29 Aug 2024 18:17:53 -0400 Subject: [PATCH 2/9] Initial working version --- .../0060_studentteam_slack_channel.py | 17 +++++ LearningAPI/models/people/student_team.py | 1 + LearningAPI/utils.py | 40 ++++++++++ LearningAPI/views/team_maker_view.py | 73 ++++++++++++++++--- 4 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 LearningAPI/migrations/0060_studentteam_slack_channel.py diff --git a/LearningAPI/migrations/0060_studentteam_slack_channel.py b/LearningAPI/migrations/0060_studentteam_slack_channel.py new file mode 100644 index 0000000..db3c926 --- /dev/null +++ b/LearningAPI/migrations/0060_studentteam_slack_channel.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.14 on 2024-08-29 20:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("LearningAPI", "0059_remove_groupprojectrepository_repository_url"), + ] + + operations = [ + migrations.AddField( + model_name="studentteam", + name="slack_channel", + field=models.CharField(default="", max_length=55), + ), + ] diff --git a/LearningAPI/models/people/student_team.py b/LearningAPI/models/people/student_team.py index 85b9c4d..89d0f51 100644 --- a/LearningAPI/models/people/student_team.py +++ b/LearningAPI/models/people/student_team.py @@ -5,6 +5,7 @@ class StudentTeam(models.Model): group_name = models.CharField(max_length=55) cohort = models.ForeignKey("Cohort", on_delete=models.CASCADE) sprint_team = models.BooleanField(default=False) + slack_channel = models.CharField(max_length=55, default="") students = models.ManyToManyField("NSSUser", through="NSSUserTeam") diff --git a/LearningAPI/utils.py b/LearningAPI/utils.py index a1788dd..2400f5a 100644 --- a/LearningAPI/utils.py +++ b/LearningAPI/utils.py @@ -4,6 +4,46 @@ import requests from requests.exceptions import ConnectionError +from LearningAPI.models.people import NssUser + +class SlackChannel(object): + """ This class is used to create a Slack channel for a student team """ + def __init__(self, name): + self.headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + self.channel_payload = { + "name": name, + "token": os.getenv("SLACK_BOT_TOKEN") + } + + def create(self, members): + # Create a Slack channel with the given name + res = requests.post("https://slack.com/api/conversations.create", timeout=10, data=self.channel_payload, headers=self.headers) + channel_res = res.json() + + # Create a set of Slack IDs for the members to be added to the channel + member_slack_ids = set() + for member_id in members: + member = NssUser.objects.get(pk=member_id) + if member.slack_handle is not None: + member_slack_ids.add(member.slack_handle) + + # Create a payload to invite students and instructors to the channel + invitation_payload = { + "channel": channel_res["channel"]["id"], + "users": ",".join(list(member_slack_ids)), + "token": os.getenv("SLACK_BOT_TOKEN") + } + + # Invite students and instructors to the channel + requests.post("https://slack.com/api/conversations.invite", timeout=10, data=invitation_payload, headers=self.headers) + + # Return the channel ID for the team + return channel_res["channel"]["id"] + + + class GithubRequest(object): def __init__(self): diff --git a/LearningAPI/views/team_maker_view.py b/LearningAPI/views/team_maker_view.py index 5a106b1..10aec32 100644 --- a/LearningAPI/views/team_maker_view.py +++ b/LearningAPI/views/team_maker_view.py @@ -1,10 +1,14 @@ -import os -import requests +import logging from rest_framework import serializers, status from rest_framework.viewsets import ViewSet from rest_framework.response import Response -from LearningAPI.models.people import StudentTeam, GroupProjectRepository, NSSUserTeam, NssUser +from LearningAPI.models.people import StudentTeam, GroupProjectRepository, NSSUserTeam, Cohort +from LearningAPI.models.coursework import Project + +from LearningAPI.utils import GithubRequest, SlackChannel +from LearningAPI.views.notify import slack_notify +import random, string class StudentTeamSerializer(serializers.ModelSerializer): @@ -24,16 +28,23 @@ def create(self, request): cohort_id = request.data.get('cohort', None) student_list = request.data.get('students', None) group_project_id = request.data.get('groupProject', None) + team_prefix = request.data.get('weeklyPrefix', None) + team_index = request.data.get('teamIndex', None) # Create the student team in the database + cohort = Cohort.objects.get(pk=cohort_id) team = StudentTeam() team.group_name = "" - team.cohort_id = cohort_id + team.cohort = cohort team.sprint_team = True if group_project_id is not None else False - team.save() + # Create the Slack channel and add students to it and store the channel ID in the team + slack_channel = SlackChannel(f"{team_prefix}-{team_index}") + created_channel_id = slack_channel.create(student_list) + team.slack_channel = created_channel_id + team.save() - # Assign the students to the team + # Assign the students to the team. Use a for loop with enumerate to get the index of the student for student in student_list: student_team = NSSUserTeam() student_team.student_id = student @@ -42,12 +53,56 @@ def create(self, request): # Create group project repository if group project is not None if group_project_id is not None: + project = Project.objects.get(pk=group_project_id) group_project_repo = GroupProjectRepository() group_project_repo.team_id = team.id - group_project_repo.project_id = group_project_id + group_project_repo.project = project group_project_repo.save() - serialized_team = StudentTeamSerializer(team, many=False).data - return Response(serialized_team, status=status.HTTP_201_CREATED) + # Create the Github repository for the group project + gh_request = GithubRequest() + client_full_url = project.client_template_url + + # Split the full URL on '/' and get the last two items + ( org, repo, ) = client_full_url.split('/')[-2:] + + # Construct request body for creating the repository + student_org_name = cohort.info.student_organization_url.split("/")[-1] + + # Replace all spaces in the assessment name with hyphens + random_suffix = ''.join(random.choice(string.ascii_lowercase) for i in range(5)) + repo_name = f'{project.name.replace(" ", "-")}-{random_suffix}' + request_body = { + "owner": student_org_name, + "name": repo_name, + "description": f"This is your client-side repository for the {project.name} sprint(s).", + "include_all_branches": False, + "private": False + } + # Create the repository + response = gh_request.post(url=f'https://api.github.com/repos/{org}/{repo}/generate',data=request_body) + + # Assign the student write permissions to the repository + request_body = { "permission":"write" } + + for student in team.students.all(): + response = gh_request.put( + url=f'https://api.github.com/repos/{student_org_name}/{repo_name}/collaborators/{student.github_handle}', + data=request_body + ) + if response.status_code != 204: + logger = logging.getLogger("LearningPlatform") + logger.exception(f"Error: {student.full_name} was not added as a collaborator to the assessment repository.") + + # Send message to student + created_repo_url = f'https://github.com/{student_org_name}/{repo_name}' + slack_notify( + f"🐙 Your client repository has been created. Visit the URL below and clone the project to your machine.\n\n{created_repo_url}", + team.slack_channel + ) + + serialized_team = StudentTeamSerializer(team, many=False).data + + return Response(serialized_team, status=status.HTTP_201_CREATED) From 2270a00141911d8d4df8cca353b73803a56c65ec Mon Sep 17 00:00:00 2001 From: Steve Brownlee Date: Thu, 29 Aug 2024 20:58:39 -0400 Subject: [PATCH 3/9] Consolidation of all Slack messaging operations --- LearningAPI/utils.py | 97 ++++++++++++++++++++++++---- LearningAPI/views/__init__.py | 2 - LearningAPI/views/notify.py | 50 +++++--------- LearningAPI/views/slack.py | 49 -------------- LearningAPI/views/slack_message.py | 35 ---------- LearningAPI/views/student_view.py | 18 +++--- LearningAPI/views/team_maker_view.py | 58 +++++++---------- LearningPlatform/urls.py | 40 ++++++------ 8 files changed, 153 insertions(+), 196 deletions(-) delete mode 100644 LearningAPI/views/slack.py delete mode 100644 LearningAPI/views/slack_message.py diff --git a/LearningAPI/utils.py b/LearningAPI/utils.py index 2400f5a..faafa12 100644 --- a/LearningAPI/utils.py +++ b/LearningAPI/utils.py @@ -1,12 +1,13 @@ import json import time import os +import logging import requests from requests.exceptions import ConnectionError from LearningAPI.models.people import NssUser -class SlackChannel(object): +class SlackAPI(object): """ This class is used to create a Slack channel for a student team """ def __init__(self, name): self.headers = { @@ -17,7 +18,26 @@ def __init__(self, name): "token": os.getenv("SLACK_BOT_TOKEN") } - def create(self, members): + def send_message(self, channel, text): + # Configure the config for the Slack message + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + channel_payload = { + "text": text, + "token": os.getenv("SLACK_BOT_TOKEN"), + "channel": channel + } + + response = requests.post( + url="https://slack.com/api/chat.postMessage", + data=channel_payload, + headers=headers + ) + return response.json() + + + def create_channel(self, members): # Create a Slack channel with the given name res = requests.post("https://slack.com/api/conversations.create", timeout=10, data=self.channel_payload, headers=self.headers) channel_res = res.json() @@ -43,8 +63,6 @@ def create(self, members): return channel_res["channel"]["id"] - - class GithubRequest(object): def __init__(self): self.headers = { @@ -55,23 +73,79 @@ def __init__(self): "Authorization": f'Bearer {os.getenv("GITHUB_TOKEN")}' } + def create_repository(self, source_url: str, student_org_url: str, repo_name: str, project_name: str) -> requests.Response: + """Create a repository for a student team + + Args: + source_url (str): The URL of the source repository + student_org_url (str): The URL of the student organization + repo_name (str): The name of the repository + project_name (str): The name of the project + + Returns: + requests.Response: The response from the GitHub API + """ + + # Split the full URL on '/' and get the last two items + ( org, repo, ) = source_url.split('/')[-2:] + + student_org_name = student_org_url.split("/")[-1] + + # Construct request body for creating the repository + request_body = { + "owner": student_org_name, + "name": repo_name, + "description": f"This is your client-side repository for the {project_name} sprint(s).", + "include_all_branches": False, + "private": False + } + + # Create the repository + response = self.post(url=f'https://api.github.com/repos/{org}/{repo}/generate', data=request_body) + + return response + + def assign_student_permissions(self, student_org_name: str, repo_name: str, student: NssUser, permission: str = "write") -> requests.Response: + """Assign write permissions to a student for a repository + + Args: + student_org_name (str): The name of the student organization + repo_name (str): The name of the repository + student (NSSUser): The student to assign permissions to + + Returns: + requests.Response: The response from the GitHub API + """ + + # Construct request body for assigning permissions to the student + request_body = { "permission":permission } + + # Assign the student write permissions to the repository + response = self.put( + url=f'https://api.github.com/repos/{student_org_name}/{repo_name}/collaborators/{student.github_handle}', + data=request_body + ) + + if response.status_code != 204: + logger = logging.getLogger("LearningPlatform") + logger.exception(f"Error: {student.full_name} was not added as a collaborator to the assessment repository.") + + return response + + def get(self, url): - return self.request_with_retry( - lambda: requests.get(url=url, headers=self.headers)) + return self.request_with_retry(lambda: requests.get(url=url, headers=self.headers)) def put(self, url, data): json_data = json.dumps(data) - return self.request_with_retry( - lambda: requests.put(url=url, data=json_data, headers=self.headers)) + return self.request_with_retry(lambda: requests.put(url=url, data=json_data, headers=self.headers)) def post(self, url, data): json_data = json.dumps(data) try: - result = self.request_with_retry( - lambda: requests.post(url=url, data=json_data, headers=self.headers)) - + result = self.request_with_retry(lambda: requests.post(url=url, data=json_data, headers=self.headers)) return result except TimeoutError: @@ -104,3 +178,4 @@ def sleep_with_countdown(self, countdown_seconds): if count: time.sleep(0.5) + diff --git a/LearningAPI/views/__init__.py b/LearningAPI/views/__init__.py index 5be95d0..ce019b5 100644 --- a/LearningAPI/views/__init__.py +++ b/LearningAPI/views/__init__.py @@ -16,8 +16,6 @@ from .github_login import GithubLogin from .student_assessment import StudentAssessmentView from .assessment_status import AssessmentStatusView -from .slack import SlackChannel -from .slack_message import SlackMessage from .core_skill_view import CoreSkillViewSet from .core_skill_record_view import CoreSkillRecordViewSet from .student_personality_view import StudentPersonalityViewSet diff --git a/LearningAPI/views/notify.py b/LearningAPI/views/notify.py index 719459e..c9cfc23 100644 --- a/LearningAPI/views/notify.py +++ b/LearningAPI/views/notify.py @@ -1,35 +1,7 @@ -import os -import requests from rest_framework.decorators import api_view from rest_framework.response import Response from LearningAPI.models.people import NssUser - -def slack_notify(message, channel): - """ - Sends a notification message to a Slack channel. - - Args: - message (str): The message to send. - channel (str): The Slack channel to send the message to. - - Raises: - requests.exceptions.Timeout: If the request to the Slack API times out. - """ - - headers = { - "Content-Type": "application/x-www-form-urlencoded" - } - - requests.post( - "https://slack.com/api/chat.postMessage", - data={ - "text": message, - "token": os.getenv("SLACK_BOT_TOKEN"), - "channel": channel - }, - headers=headers, - timeout=10 - ) +from LearningAPI.utils import SlackAPI @api_view(['POST']) def notify(request): @@ -51,10 +23,20 @@ def notify(request): requests.exceptions.Timeout: If the request to the Slack API times out. """ - student = NssUser.objects.get(user=request.auth.user) - slack_channel = student.assigned_cohorts.order_by("-id").first().cohort.slack_channel + instructors = request.data.get("instructors", None) + student_channel = request.data.get("studentChannel", None) + message = request.data.get("message", None) + + if instructors: + # Get the cohort's instrutor Slack channel + target_user = NssUser.objects.get(user=request.auth.user) + slack_channel = target_user.assigned_cohorts.order_by("-id").first().cohort.slack_channel + SlackAPI().send_message( text=message, channel=slack_channel ) + return Response({ 'message': 'Notification sent to instructor channel'}, status=200) - message = request.data.get("message") - slack_notify(message, slack_channel) + elif student_channel is not None: + slack_channel = target_user.assigned_cohorts.order_by("-id").first().cohort.slack_channel + SlackAPI().send_message( text=message, channel=student_channel ) + return Response({ 'message': 'Notification sent to student'}, status=200) - return Response({ 'message': 'Notification sent to Slack!'}, status=200) + return Response({ 'message': 'Invalid request'}, status=400) diff --git a/LearningAPI/views/slack.py b/LearningAPI/views/slack.py deleted file mode 100644 index abf1b9c..0000000 --- a/LearningAPI/views/slack.py +++ /dev/null @@ -1,49 +0,0 @@ -"""View module for handling requests about park areas""" -import os -import requests -from rest_framework import status -from rest_framework.viewsets import ViewSet -from rest_framework.response import Response -from LearningAPI.models.people import NssUser - - -class SlackChannel(ViewSet): - """For creating Slack channels""" - - def create(self, request): - """Handle POST requests to create team Slack channels""" - - # Create the Slack channel - headers = { - "Content-Type": "application/x-www-form-urlencoded" - } - channel_payload = { - "name": request.data["name"], - "token": os.getenv("SLACK_BOT_TOKEN") - } - - student_slack_ids = set() - for student_id in request.data["students"]: - student = NssUser.objects.get(pk=student_id) - if student.slack_handle is not None: - student_slack_ids.add(student.slack_handle) - - res = requests.post("https://slack.com/api/conversations.create", timeout=10, data=channel_payload, headers=headers) - channel_res = res.json() - - # Add students to Slack channel - invitation_payload = { - "channel": channel_res["channel"]["id"], - "users": ",".join(list(student_slack_ids)), - "token": os.getenv("SLACK_BOT_TOKEN") - } - - res = requests.post("https://slack.com/api/conversations.invite", timeout=10, data=invitation_payload, headers=headers) - students_res = res.json() - - combined_response = { - "channel": channel_res, - "invitations": students_res - } - - return Response(combined_response, status=status.HTTP_201_CREATED) diff --git a/LearningAPI/views/slack_message.py b/LearningAPI/views/slack_message.py deleted file mode 100644 index e45177d..0000000 --- a/LearningAPI/views/slack_message.py +++ /dev/null @@ -1,35 +0,0 @@ -"""View module for handling requests about park areas""" -import os -import requests -from rest_framework import status -from rest_framework.viewsets import ViewSet -from rest_framework.response import Response - - -class SlackMessage(ViewSet): - """For creating Slack channels""" - - def create(self, request): - """Handle POST requests to create team Slack channels""" - - student_id = request.data.get("student", None) - - if student_id is not None: - - # Create the Slack channel - headers = { - "Content-Type": "application/x-www-form-urlencoded" - } - channel_payload = { - "text": request.data.get("text", "Test message"), - "token": os.getenv("SLACK_BOT_TOKEN"), - "channel": student_id - } - - res = requests.post( - "https://slack.com/api/chat.postMessage", data=channel_payload, headers=headers) - message_response = res.json() - - return Response(message_response, status=status.HTTP_201_CREATED) - - return Response(None, status=status.HTTP_400_BAD_REQUEST) diff --git a/LearningAPI/views/student_view.py b/LearningAPI/views/student_view.py index 8ddcb58..7cd39bf 100644 --- a/LearningAPI/views/student_view.py +++ b/LearningAPI/views/student_view.py @@ -14,17 +14,15 @@ from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet -from LearningAPI.utils import GithubRequest +from LearningAPI.utils import GithubRequest, SlackAPI from LearningAPI.decorators import is_instructor from LearningAPI.models import Tag from LearningAPI.models.coursework import StudentProject, Project, Capstone, CapstoneTimeline from LearningAPI.models.people import (StudentNote, NssUser, StudentAssessment, OneOnOneNote, StudentPersonality, Assessment, - StudentAssessmentStatus, StudentTag - ) + StudentAssessmentStatus, StudentTag) from LearningAPI.models.skill import (CoreSkillRecord, LearningRecord, LearningRecordEntry) -from LearningAPI.views.notify import slack_notify from .personality import myers_briggs_persona @@ -241,6 +239,8 @@ def list(self, request): def assess(self, request, pk): """POST when a student starts working on book assessment. PUT to change status.""" + slack = SlackAPI() + if request.method == "PUT": student = NssUser.objects.get(pk=pk) assessment_status = StudentAssessmentStatus.objects.get(pk=request.data['statusId']) @@ -252,18 +252,18 @@ def assess(self, request, pk): try: if latest_assessment.status.status == 'Ready for Review': - slack_notify( + slack.send_message( message="🎉 Congratulations! You've completed your self-assessment. Your coaching team will review your work and provide feedback soon.", channel=student.slack_handle ) - slack_notify( + slack.send_message( message=f'{student.full_name} in {student.current_cohort["name"]} has completed their self-assessment for {latest_assessment.assessment.name}.\n\nReview it at {latest_assessment.url}', channel=student.current_cohort["ic"] ) if latest_assessment.status.status == 'Reviewed and Complete': - slack_notify( + slack.send_message( message=f':fox-yay-woo-hoo: Self-Assessment Review Complete\n\n\n:white_check_mark: Your coaching team just marked {latest_assessment.assessment.name} as completed.\n\nVisit https://learning.nss.team to view your latest messages and statuses.', channel=latest_assessment.student.slack_handle ) @@ -362,14 +362,14 @@ def assess(self, request, pk): # Send message to student created_repo_url = f'https://github.com/{student_org_name}/{repo_name}' - slack_notify( + slack.send_message( f"🐙 Your self-assessment repository has been created. Visit the URL below and clone the project to your machine.\n\n{created_repo_url}", student.slack_handle ) # Send message to instructors slack_channel = student.assigned_cohorts.order_by("-id").first().cohort.slack_channel - slack_notify( + slack.send_message( f"📝 {student.full_name} has started the self-assessment for {assessment.name}.", slack_channel ) diff --git a/LearningAPI/views/team_maker_view.py b/LearningAPI/views/team_maker_view.py index 10aec32..67f6ba6 100644 --- a/LearningAPI/views/team_maker_view.py +++ b/LearningAPI/views/team_maker_view.py @@ -1,14 +1,12 @@ -import logging +import random, string from rest_framework import serializers, status from rest_framework.viewsets import ViewSet from rest_framework.response import Response + from LearningAPI.models.people import StudentTeam, GroupProjectRepository, NSSUserTeam, Cohort from LearningAPI.models.coursework import Project - -from LearningAPI.utils import GithubRequest, SlackChannel -from LearningAPI.views.notify import slack_notify -import random, string +from LearningAPI.utils import GithubRequest, SlackAPI class StudentTeamSerializer(serializers.ModelSerializer): @@ -25,6 +23,8 @@ def create(self, request): Returns: Response -- JSON serialized instance """ + slack = SlackAPI() + cohort_id = request.data.get('cohort', None) student_list = request.data.get('students', None) group_project_id = request.data.get('groupProject', None) @@ -39,7 +39,7 @@ def create(self, request): team.sprint_team = True if group_project_id is not None else False # Create the Slack channel and add students to it and store the channel ID in the team - slack_channel = SlackChannel(f"{team_prefix}-{team_index}") + slack_channel = slack.create_channel(f"{team_prefix}-{team_index}") created_channel_id = slack_channel.create(student_list) team.slack_channel = created_channel_id team.save() @@ -59,48 +59,36 @@ def create(self, request): group_project_repo.project = project group_project_repo.save() - # Create the Github repository for the group project - gh_request = GithubRequest() - client_full_url = project.client_template_url - - # Split the full URL on '/' and get the last two items - ( org, repo, ) = client_full_url.split('/')[-2:] - - # Construct request body for creating the repository + # Get student Github organization name student_org_name = cohort.info.student_organization_url.split("/")[-1] # Replace all spaces in the assessment name with hyphens random_suffix = ''.join(random.choice(string.ascii_lowercase) for i in range(5)) repo_name = f'{project.name.replace(" ", "-")}-{random_suffix}' - request_body = { - "owner": student_org_name, - "name": repo_name, - "description": f"This is your client-side repository for the {project.name} sprint(s).", - "include_all_branches": False, - "private": False - } - - # Create the repository - response = gh_request.post(url=f'https://api.github.com/repos/{org}/{repo}/generate',data=request_body) + # Create the client repository for the group project + gh_request = GithubRequest() - # Assign the student write permissions to the repository - request_body = { "permission":"write" } + gh_request.create_repository( + source_url=project.client_template_url, + student_org_url=cohort.info.student_organization_url, + repo_name=repo_name, + project_name=project.name + ) + # Grant write permissions to the students for student in team.students.all(): - response = gh_request.put( - url=f'https://api.github.com/repos/{student_org_name}/{repo_name}/collaborators/{student.github_handle}', - data=request_body + gh_request.assign_student_permissions( + student_org_name=student_org_name, + repo_name=repo_name, + student=student ) - if response.status_code != 204: - logger = logging.getLogger("LearningPlatform") - logger.exception(f"Error: {student.full_name} was not added as a collaborator to the assessment repository.") # Send message to student created_repo_url = f'https://github.com/{student_org_name}/{repo_name}' - slack_notify( - f"🐙 Your client repository has been created. Visit the URL below and clone the project to your machine.\n\n{created_repo_url}", - team.slack_channel + slack.send_message( + text=f"🐙 Your client repository has been created. Visit the URL below and clone the project to your machine.\n\n{created_repo_url}", + channel=team.slack_channel ) serialized_team = StudentTeamSerializer(team, many=False).data diff --git a/LearningPlatform/urls.py b/LearningPlatform/urls.py index 10dbad9..bb5f06a 100644 --- a/LearningPlatform/urls.py +++ b/LearningPlatform/urls.py @@ -22,34 +22,32 @@ from LearningAPI import views router = routers.DefaultRouter(trailing_slash=False) -router.register(r'profile', views.Profile, 'profile') -router.register(r'personality', views.StudentPersonalityViewSet, 'personality') -router.register(r'records', views.LearningRecordViewSet, 'record') -router.register(r'weights', views.LearningWeightViewSet, 'weight') -router.register(r'opportunities', views.OpportunityViewSet, 'opportunity') -router.register(r'capstones', views.CapstoneViewSet, 'capstone') -router.register(r'cohorts', views.CohortViewSet, 'cohort') -router.register(r'students', views.StudentViewSet, 'student') -router.register(r'courses', views.CourseViewSet, 'course') -router.register(r'books', views.BookViewSet, 'book') -router.register(r'projects', views.ProjectViewSet, 'project') router.register(r'assessments', views.StudentAssessmentView, 'assessment') router.register(r'bookassessments', views.BookAssessmentView, 'bookassessment') -router.register(r'statuses', views.AssessmentStatusView, 'status') -router.register(r'proposalstatuses', views.ProposalStatusView, 'proposalstatus') -router.register(r'timelines', views.TimelineView, 'timeline') -router.register(r'objectives', views.LearningObjectiveViewSet, 'learningobjective') -router.register(r'slackchannels', views.SlackChannel, 'slackchannel') -router.register(r'messages', views.SlackMessage, 'slackmessage') -router.register(r'coreskills', views.CoreSkillViewSet, 'coreskill') +router.register(r'books', views.BookViewSet, 'book') +router.register(r'capstones', views.CapstoneViewSet, 'capstone') +router.register(r'cohortinfo', views.CohortInfoViewSet, 'info') +router.register(r'cohorts', views.CohortViewSet, 'cohort') router.register(r'coreskillrecords', views.CoreSkillRecordViewSet, 'coreskillrecord') -router.register(r'tags', views.TagViewSet, 'tag') -router.register(r'studenttags', views.StudentTagViewSet, 'studenttag') +router.register(r'coreskills', views.CoreSkillViewSet, 'coreskill') +router.register(r'courses', views.CourseViewSet, 'course') router.register(r'notes', views.StudentNoteViewSet, 'note') router.register(r'notetypes', views.StudentNoteTypeViewSet, 'notetypes') +router.register(r'objectives', views.LearningObjectiveViewSet, 'learningobjective') +router.register(r'opportunities', views.OpportunityViewSet, 'opportunity') router.register(r'personalities', views.PersonalityView, 'person') -router.register(r'cohortinfo', views.CohortInfoViewSet, 'info') +router.register(r'profile', views.Profile, 'profile') +router.register(r'projects', views.ProjectViewSet, 'project') +router.register(r'proposalstatuses', views.ProposalStatusView, 'proposalstatus') +router.register(r'personality', views.StudentPersonalityViewSet, 'personality') +router.register(r'records', views.LearningRecordViewSet, 'record') +router.register(r'statuses', views.AssessmentStatusView, 'status') +router.register(r'students', views.StudentViewSet, 'student') +router.register(r'studenttags', views.StudentTagViewSet, 'studenttag') +router.register(r'tags', views.TagViewSet, 'tag') router.register(r'teams', views.TeamMakerView, 'team_maker') +router.register(r'timelines', views.TimelineView, 'timeline') +router.register(r'weights', views.LearningWeightViewSet, 'weight') urlpatterns = [ From 91499a9a7bdb51a91f2367aea15b098eb233ae99 Mon Sep 17 00:00:00 2001 From: Steve Brownlee Date: Fri, 30 Aug 2024 17:28:28 -0400 Subject: [PATCH 4/9] Support updating a project with client/api urls --- LearningAPI/utils.py | 26 +++++++++-------- LearningAPI/views/project_view.py | 6 ++-- LearningAPI/views/team_maker_view.py | 42 ++++++++++++++++++++++------ 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/LearningAPI/utils.py b/LearningAPI/utils.py index faafa12..43e1075 100644 --- a/LearningAPI/utils.py +++ b/LearningAPI/utils.py @@ -9,14 +9,10 @@ class SlackAPI(object): """ This class is used to create a Slack channel for a student team """ - def __init__(self, name): + def __init__(self): self.headers = { "Content-Type": "application/x-www-form-urlencoded" } - self.channel_payload = { - "name": name, - "token": os.getenv("SLACK_BOT_TOKEN") - } def send_message(self, channel, text): # Configure the config for the Slack message @@ -32,14 +28,22 @@ def send_message(self, channel, text): response = requests.post( url="https://slack.com/api/chat.postMessage", data=channel_payload, - headers=headers + headers=headers, + timeout=10 ) return response.json() - def create_channel(self, members): + def create_channel(self, name, members): + """Create a Slack channel for a student team""" + + channel_payload = { + "name": name, + "token": os.getenv("SLACK_BOT_TOKEN") + } + # Create a Slack channel with the given name - res = requests.post("https://slack.com/api/conversations.create", timeout=10, data=self.channel_payload, headers=self.headers) + res = requests.post("https://slack.com/api/conversations.create", timeout=10, data=channel_payload, headers=self.headers) channel_res = res.json() # Create a set of Slack IDs for the members to be added to the channel @@ -134,18 +138,18 @@ def assign_student_permissions(self, student_org_name: str, repo_name: str, stud def get(self, url): - return self.request_with_retry(lambda: requests.get(url=url, headers=self.headers)) + return self.request_with_retry(lambda: requests.get(url=url, headers=self.headers, timeout=10)) def put(self, url, data): json_data = json.dumps(data) - return self.request_with_retry(lambda: requests.put(url=url, data=json_data, headers=self.headers)) + return self.request_with_retry(lambda: requests.put(url=url, data=json_data, headers=self.headers, timeout=10)) def post(self, url, data): json_data = json.dumps(data) try: - result = self.request_with_retry(lambda: requests.post(url=url, data=json_data, headers=self.headers)) + result = self.request_with_retry(lambda: requests.post(url=url, data=json_data, headers=self.headers, timeout=10)) return result except TimeoutError: diff --git a/LearningAPI/views/project_view.py b/LearningAPI/views/project_view.py index c81336f..1c4f1bd 100644 --- a/LearningAPI/views/project_view.py +++ b/LearningAPI/views/project_view.py @@ -54,15 +54,15 @@ def update(self, request, pk=None): Returns: Response -- Empty body with 204 status code """ - url = request.data.get("implementation_url", "") - try: project = Project.objects.get(pk=pk) project.name = request.data["name"] project.active = request.data["active"] project.index = request.data["index"] project.is_group_project = request.data["is_group_project"] - project.implementation_url = url + project.api_template_url = request.data.get("api_template_url", "") + project.client_template_url = request.data.get("client_template_url", "") + project.implementation_url = request.data.get("implementation_url", "") project.save() except Project.DoesNotExist: diff --git a/LearningAPI/views/team_maker_view.py b/LearningAPI/views/team_maker_view.py index 67f6ba6..86e1c78 100644 --- a/LearningAPI/views/team_maker_view.py +++ b/LearningAPI/views/team_maker_view.py @@ -23,7 +23,6 @@ def create(self, request): Returns: Response -- JSON serialized instance """ - slack = SlackAPI() cohort_id = request.data.get('cohort', None) student_list = request.data.get('students', None) @@ -36,12 +35,12 @@ def create(self, request): team = StudentTeam() team.group_name = "" team.cohort = cohort - team.sprint_team = True if group_project_id is not None else False + team.sprint_team = group_project_id is not None # Create the Slack channel and add students to it and store the channel ID in the team - slack_channel = slack.create_channel(f"{team_prefix}-{team_index}") - created_channel_id = slack_channel.create(student_list) - team.slack_channel = created_channel_id + slack = SlackAPI() + channel_name = f"{team_prefix}-{cohort.name.replace(' ', '-').lower()}-{team_index}" + team.slack_channel = slack.create_channel(channel_name, student_list) team.save() # Assign the students to the team. Use a for loop with enumerate to get the index of the student @@ -63,8 +62,8 @@ def create(self, request): student_org_name = cohort.info.student_organization_url.split("/")[-1] # Replace all spaces in the assessment name with hyphens - random_suffix = ''.join(random.choice(string.ascii_lowercase) for i in range(5)) - repo_name = f'{project.name.replace(" ", "-")}-{random_suffix}' + random_suffix = ''.join(random.choice(string.ascii_lowercase) for i in range(6)) + repo_name = f'{project.name.replace(" ", "-")}-client-{random_suffix}' # Create the client repository for the group project gh_request = GithubRequest() @@ -84,13 +83,40 @@ def create(self, request): student=student ) - # Send message to student + # Send message to project team's Slack channel with the repository URL created_repo_url = f'https://github.com/{student_org_name}/{repo_name}' slack.send_message( text=f"🐙 Your client repository has been created. Visit the URL below and clone the project to your machine.\n\n{created_repo_url}", channel=team.slack_channel ) + + # Create the API repository for the group project if it exists + if project.api_template_url: + api_repo_name = f'{project.name.replace(" ", "-")}-api-{random_suffix}' + + gh_request.create_repository( + source_url=project.api_template_url, + student_org_url=cohort.info.student_organization_url, + repo_name=api_repo_name, + project_name=project.name + ) + + # Grant write permissions to the students + for student in team.students.all(): + gh_request.assign_student_permissions( + student_org_name=student_org_name, + repo_name=api_repo_name, + student=student + ) + + # Send message to project team's Slack channel with the repository URL + created_repo_url = f'https://github.com/{student_org_name}/{api_repo_name}' + slack.send_message( + text=f"🐙 Your API repository has been created. Visit the URL below and clone the project to your machine.\n\n{created_repo_url}", + channel=team.slack_channel + ) + serialized_team = StudentTeamSerializer(team, many=False).data return Response(serialized_team, status=status.HTTP_201_CREATED) From 1ac537b8e700dad32d8a20cbd71461ba8a6607c5 Mon Sep 17 00:00:00 2001 From: Steve Brownlee Date: Tue, 3 Sep 2024 11:34:28 -0400 Subject: [PATCH 5/9] Get teams for cohort. Delete teams for cohort. --- ...squashed_0060_studentteam_slack_channel.py | 138 ++++++++++++++++++ LearningAPI/models/people/nssuser.py | 4 + LearningAPI/models/people/student_team.py | 2 - LearningAPI/utils.py | 24 ++- LearningAPI/views/team_maker_view.py | 32 ++++ learopsdev.session.sql | 45 ++---- 6 files changed, 202 insertions(+), 43 deletions(-) create mode 100644 LearningAPI/migrations/0052_studentteam_nssuserteam_groupprojectrepository_squashed_0060_studentteam_slack_channel.py diff --git a/LearningAPI/migrations/0052_studentteam_nssuserteam_groupprojectrepository_squashed_0060_studentteam_slack_channel.py b/LearningAPI/migrations/0052_studentteam_nssuserteam_groupprojectrepository_squashed_0060_studentteam_slack_channel.py new file mode 100644 index 0000000..bf9e357 --- /dev/null +++ b/LearningAPI/migrations/0052_studentteam_nssuserteam_groupprojectrepository_squashed_0060_studentteam_slack_channel.py @@ -0,0 +1,138 @@ +# Generated by Django 4.2.14 on 2024-09-03 01:49 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + replaces = [ + ("LearningAPI", "0052_studentteam_nssuserteam_groupprojectrepository"), + ("LearningAPI", "0053_alter_studentteam_group_name"), + ("LearningAPI", "0054_alter_nssuserteam_student_alter_nssuserteam_team"), + ("LearningAPI", "0055_studentteam_members_alter_nssuserteam_student_and_more"), + ("LearningAPI", "0056_rename_members_studentteam_students"), + ("LearningAPI", "0057_project_template_url"), + ( + "LearningAPI", + "0058_remove_project_template_url_project_api_template_url_and_more", + ), + ("LearningAPI", "0059_remove_groupprojectrepository_repository_url"), + ("LearningAPI", "0060_studentteam_slack_channel"), + ] + + dependencies = [ + ("LearningAPI", "0051_add_student_note_type_to_database_function"), + ] + + operations = [ + migrations.CreateModel( + name="StudentTeam", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("group_name", models.CharField(max_length=55)), + ("sprint_team", models.BooleanField(default=False)), + ( + "cohort", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="LearningAPI.cohort", + ), + ), + ], + ), + migrations.CreateModel( + name="NSSUserTeam", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "student", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="LearningAPI.nssuser", + ), + ), + ( + "team", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="members", + to="LearningAPI.studentteam", + ), + ), + ], + ), + migrations.AddField( + model_name="studentteam", + name="students", + field=models.ManyToManyField( + through="LearningAPI.NSSUserTeam", to="LearningAPI.nssuser" + ), + ), + migrations.AlterField( + model_name="nssuserteam", + name="team", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="LearningAPI.studentteam", + ), + ), + migrations.AddField( + model_name="project", + name="api_template_url", + field=models.URLField(default=""), + ), + migrations.AddField( + model_name="project", + name="client_template_url", + field=models.URLField(default=""), + ), + migrations.CreateModel( + name="GroupProjectRepository", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="LearningAPI.project", + ), + ), + ( + "team", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="LearningAPI.studentteam", + ), + ), + ], + ), + migrations.AddField( + model_name="studentteam", + name="slack_channel", + field=models.CharField(default="", max_length=55), + ), + ] diff --git a/LearningAPI/models/people/nssuser.py b/LearningAPI/models/people/nssuser.py index fdd0899..8468bab 100644 --- a/LearningAPI/models/people/nssuser.py +++ b/LearningAPI/models/people/nssuser.py @@ -25,6 +25,10 @@ def __str__(self) -> str: def full_name(self): return f'{self.user.first_name} {self.user.last_name}' + @property + def name(self): + return f'{self.user.first_name} {self.user.last_name}' + @property def score(self): """Return total learning score""" diff --git a/LearningAPI/models/people/student_team.py b/LearningAPI/models/people/student_team.py index 89d0f51..fb1006b 100644 --- a/LearningAPI/models/people/student_team.py +++ b/LearningAPI/models/people/student_team.py @@ -7,5 +7,3 @@ class StudentTeam(models.Model): sprint_team = models.BooleanField(default=False) slack_channel = models.CharField(max_length=55, default="") students = models.ManyToManyField("NSSUser", through="NSSUserTeam") - - diff --git a/LearningAPI/utils.py b/LearningAPI/utils.py index 43e1075..9478978 100644 --- a/LearningAPI/utils.py +++ b/LearningAPI/utils.py @@ -16,9 +16,6 @@ def __init__(self): def send_message(self, channel, text): # Configure the config for the Slack message - headers = { - "Content-Type": "application/x-www-form-urlencoded" - } channel_payload = { "text": text, "token": os.getenv("SLACK_BOT_TOKEN"), @@ -28,7 +25,7 @@ def send_message(self, channel, text): response = requests.post( url="https://slack.com/api/chat.postMessage", data=channel_payload, - headers=headers, + headers=self.headers, timeout=10 ) return response.json() @@ -43,7 +40,12 @@ def create_channel(self, name, members): } # Create a Slack channel with the given name - res = requests.post("https://slack.com/api/conversations.create", timeout=10, data=channel_payload, headers=self.headers) + res = requests.post( + "https://slack.com/api/conversations.create", + timeout=10, + data=channel_payload, + headers=self.headers + ) channel_res = res.json() # Create a set of Slack IDs for the members to be added to the channel @@ -61,7 +63,12 @@ def create_channel(self, name, members): } # Invite students and instructors to the channel - requests.post("https://slack.com/api/conversations.invite", timeout=10, data=invitation_payload, headers=self.headers) + requests.post( + "https://slack.com/api/conversations.invite", + timeout=10, + data=invitation_payload, + headers=self.headers + ) # Return the channel ID for the team return channel_res["channel"]["id"] @@ -132,7 +139,10 @@ def assign_student_permissions(self, student_org_name: str, repo_name: str, stud if response.status_code != 204: logger = logging.getLogger("LearningPlatform") - logger.exception(f"Error: {student.full_name} was not added as a collaborator to the assessment repository.") + logger.exception( + "Error: %s was not added as a collaborator to the assessment repository.", + student.full_name + ) return response diff --git a/LearningAPI/views/team_maker_view.py b/LearningAPI/views/team_maker_view.py index 86e1c78..3afa1d8 100644 --- a/LearningAPI/views/team_maker_view.py +++ b/LearningAPI/views/team_maker_view.py @@ -10,6 +10,11 @@ class StudentTeamSerializer(serializers.ModelSerializer): + students = serializers.SerializerMethodField() + + def get_students(self, obj): + return [{'id': student.id, 'name': student.name} for student in obj.students.all() if not student.user.is_staff] + class Meta: model = StudentTeam fields = ( 'group_name', 'cohort', 'sprint_team', 'students' ) @@ -17,6 +22,18 @@ class Meta: class TeamMakerView(ViewSet): """Team Maker""" + def list(self, request): + cohort_id = request.query_params.get('cohort', None) + + try: + cohort = Cohort.objects.get(pk=cohort_id) + teams = StudentTeam.objects.filter(cohort=cohort).order_by('-pk') + + response = StudentTeamSerializer(teams, many=True).data + return Response(response, status=status.HTTP_200_OK) + except Cohort.DoesNotExist as ex: + return Response({'message': str(ex)}, status=status.HTTP_404_NOT_FOUND) + def create(self, request): """Handle POST operations @@ -120,3 +137,18 @@ def create(self, request): serialized_team = StudentTeamSerializer(team, many=False).data return Response(serialized_team, status=status.HTTP_201_CREATED) + + def destroy(self, request, pk=None): + + if pk is None: + return Response({'message': 'No cohort ID provided'}, status=status.HTTP_400_BAD_REQUEST) + + try: + cohort = Cohort.objects.get(pk=pk) + NSSUserTeam.objects.filter(team__cohort=cohort).delete() + GroupProjectRepository.objects.filter(team__cohort=cohort).delete() + StudentTeam.objects.filter(cohort=cohort).delete() + + return Response(None, status=status.HTTP_204_NO_CONTENT) + except Cohort.DoesNotExist as ex: + return Response({'message': str(ex)}, status=status.HTTP_404_NOT_FOUND) diff --git a/learopsdev.session.sql b/learopsdev.session.sql index be97e15..8ebb4a9 100644 --- a/learopsdev.session.sql +++ b/learopsdev.session.sql @@ -1,48 +1,25 @@ -- View all tables -select * from pg_catalog.pg_tables; +SELECT * from pg_catalog.pg_tables; -select * from "auth_user" order by id desc; -select * from "socialaccount_socialaccount" order by id desc; -select * from "authtoken_token" where user_id = 470; -select * from "LearningAPI_nssuser" where user_id = 470; -select * from "LearningAPI_nssusercohort" where nss_user_id = 468; +SELECT * from "auth_user" order by id desc; +SELECT * from "socialaccount_socialaccount" order by id desc; +SELECT * from "authtoken_token" where user_id = 470; +SELECT * from "LearningAPI_nssuser" where user_id = 470; +SELECT * from "LearningAPI_nssusercohort" where nss_user_id = 468; + DELETE FROM "LearningAPI_nssuserteam"; DELETE FROM "LearningAPI_groupprojectrepository"; DELETE FROM "LearningAPI_studentteam"; +SELECT * from "LearningAPI_studentteam"; --- Drop all tables -DO $$ DECLARE - r RECORD; -BEGIN - FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = current_schema()) LOOP - EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE'; - END LOOP; -END $$; -DROP FUNCTION IF EXISTS get_cohort_student_data(INT); DROP FUNCTION IF EXISTS get_student_details(INT); DROP FUNCTION IF EXISTS get_project_average_start_delay(INT); -ALTER TABLE auth_user ALTER COLUMN last_login DROP NOT NULL; - - - - - -select * FROM "LearningAPI_nssuser"; -select * FROM "LearningAPI_cohort"; - - - - - - - - DROP FUNCTION IF EXISTS get_cohort_student_data(INT); -select * from get_cohort_student_data(29); +SELECT * from get_cohort_student_data(29); @@ -367,7 +344,7 @@ SELECT * FROM "LearningAPI_learningrecordentry" lr order by id desc; -select +SELECT sum(lw.weight) as score, au."first_name" || ' ' || au."last_name" AS student_name from "LearningAPI_learningrecord" lr @@ -384,4 +361,4 @@ group by student_name -select * from "LearningAPI_studentnote" order by id desc; \ No newline at end of file +SELECT * from "LearningAPI_studentnote" order by id desc; \ No newline at end of file From 3fc24c3cc3ab0e7bab4e416453a3a61a2e3bf8af Mon Sep 17 00:00:00 2001 From: Steve Brownlee Date: Tue, 1 Oct 2024 17:23:52 -0400 Subject: [PATCH 6/9] Switched to custom action instead of standard destroy() method --- LearningAPI/views/team_maker_view.py | 39 ++++++++++++++++++++++------ learopsdev.session.sql | 1 + 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/LearningAPI/views/team_maker_view.py b/LearningAPI/views/team_maker_view.py index 3afa1d8..e29e2c1 100644 --- a/LearningAPI/views/team_maker_view.py +++ b/LearningAPI/views/team_maker_view.py @@ -3,6 +3,7 @@ from rest_framework import serializers, status from rest_framework.viewsets import ViewSet from rest_framework.response import Response +from rest_framework.decorators import action from LearningAPI.models.people import StudentTeam, GroupProjectRepository, NSSUserTeam, Cohort from LearningAPI.models.coursework import Project @@ -85,13 +86,16 @@ def create(self, request): # Create the client repository for the group project gh_request = GithubRequest() - gh_request.create_repository( + response = gh_request.create_repository( source_url=project.client_template_url, student_org_url=cohort.info.student_organization_url, repo_name=repo_name, project_name=project.name ) + if response.status_code != 201: + return Response({'message': 'Failed to create repository'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + # Grant write permissions to the students for student in team.students.all(): gh_request.assign_student_permissions( @@ -138,17 +142,36 @@ def create(self, request): return Response(serialized_team, status=status.HTTP_201_CREATED) - def destroy(self, request, pk=None): + @action(detail=False, methods=['delete']) + def reset(self, request): + cohort_id = request.query_params.get('cohort', None) - if pk is None: + if cohort_id is None: return Response({'message': 'No cohort ID provided'}, status=status.HTTP_400_BAD_REQUEST) try: - cohort = Cohort.objects.get(pk=pk) - NSSUserTeam.objects.filter(team__cohort=cohort).delete() - GroupProjectRepository.objects.filter(team__cohort=cohort).delete() - StudentTeam.objects.filter(cohort=cohort).delete() - + cohort = Cohort.objects.get(pk=cohort_id) + self._delete_cohort_teams(cohort) return Response(None, status=status.HTTP_204_NO_CONTENT) except Cohort.DoesNotExist as ex: return Response({'message': str(ex)}, status=status.HTTP_404_NOT_FOUND) + + def _delete_cohort_teams(self, cohort): + NSSUserTeam.objects.filter(team__cohort=cohort).delete() + GroupProjectRepository.objects.filter(team__cohort=cohort).delete() + StudentTeam.objects.filter(cohort=cohort).delete() + + # def destroy(self, request, pk=None): + + # if pk is None: + # return Response({'message': 'No cohort ID provided'}, status=status.HTTP_400_BAD_REQUEST) + + # try: + # cohort = Cohort.objects.get(pk=pk) + # NSSUserTeam.objects.filter(team__cohort=cohort).delete() + # GroupProjectRepository.objects.filter(team__cohort=cohort).delete() + # StudentTeam.objects.filter(cohort=cohort).delete() + + # return Response(None, status=status.HTTP_204_NO_CONTENT) + # except Cohort.DoesNotExist as ex: + # return Response({'message': str(ex)}, status=status.HTTP_404_NOT_FOUND) diff --git a/learopsdev.session.sql b/learopsdev.session.sql index 8ebb4a9..c2556a3 100644 --- a/learopsdev.session.sql +++ b/learopsdev.session.sql @@ -9,6 +9,7 @@ SELECT * from "authtoken_token" where user_id = 470; SELECT * from "LearningAPI_nssuser" where user_id = 470; SELECT * from "LearningAPI_nssusercohort" where nss_user_id = 468; +DELETE FROM "LearningAPI_cohort"; DELETE FROM "LearningAPI_nssuserteam"; DELETE FROM "LearningAPI_groupprojectrepository"; DELETE FROM "LearningAPI_studentteam"; From a3106ff64a90029cf6c2517ee195db00dda25a73 Mon Sep 17 00:00:00 2001 From: Steve Brownlee Date: Wed, 2 Oct 2024 13:07:16 -0400 Subject: [PATCH 7/9] Support getting only group projects. Include repositories in team response --- .../0061_groupprojectrepository_repository.py | 20 +++++++++++++ .../0062_alter_groupprojectrepository_team.py | 22 +++++++++++++++ .../models/people/group_project_repo.py | 3 +- LearningAPI/views/project_view.py | 4 +++ LearningAPI/views/team_maker_view.py | 28 +++++++++++++++---- learopsdev.session.sql | 6 +--- 6 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 LearningAPI/migrations/0061_groupprojectrepository_repository.py create mode 100644 LearningAPI/migrations/0062_alter_groupprojectrepository_team.py diff --git a/LearningAPI/migrations/0061_groupprojectrepository_repository.py b/LearningAPI/migrations/0061_groupprojectrepository_repository.py new file mode 100644 index 0000000..e566496 --- /dev/null +++ b/LearningAPI/migrations/0061_groupprojectrepository_repository.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.14 on 2024-10-02 11:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "LearningAPI", + "0052_studentteam_nssuserteam_groupprojectrepository_squashed_0060_studentteam_slack_channel", + ), + ] + + operations = [ + migrations.AddField( + model_name="groupprojectrepository", + name="repository", + field=models.CharField(default="", max_length=255), + ), + ] diff --git a/LearningAPI/migrations/0062_alter_groupprojectrepository_team.py b/LearningAPI/migrations/0062_alter_groupprojectrepository_team.py new file mode 100644 index 0000000..57e6e5a --- /dev/null +++ b/LearningAPI/migrations/0062_alter_groupprojectrepository_team.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.14 on 2024-10-02 16:39 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("LearningAPI", "0061_groupprojectrepository_repository"), + ] + + operations = [ + migrations.AlterField( + model_name="groupprojectrepository", + name="team", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="repositories", + to="LearningAPI.studentteam", + ), + ), + ] diff --git a/LearningAPI/models/people/group_project_repo.py b/LearningAPI/models/people/group_project_repo.py index 7b48ed7..8adae4c 100644 --- a/LearningAPI/models/people/group_project_repo.py +++ b/LearningAPI/models/people/group_project_repo.py @@ -3,8 +3,9 @@ class GroupProjectRepository(models.Model): """ This class stores the repository URLs for a group project """ - team = models.ForeignKey("StudentTeam", on_delete=models.CASCADE) + team = models.ForeignKey("StudentTeam", on_delete=models.CASCADE, related_name="repositories") project = models.ForeignKey("Project", on_delete=models.CASCADE) + repository = models.CharField(max_length=255, default="") def clean(self): super().clean() diff --git a/LearningAPI/views/project_view.py b/LearningAPI/views/project_view.py index 1c4f1bd..df3676d 100644 --- a/LearningAPI/views/project_view.py +++ b/LearningAPI/views/project_view.py @@ -99,6 +99,7 @@ def list(self, request): """ book_id = request.query_params.get("bookId", None) course_id = request.query_params.get("courseId", None) + group_projects = request.query_params.get("group", "false") try: projects = Project.objects.all().order_by('book__index', 'index') @@ -109,6 +110,9 @@ def list(self, request): if book_id is not None: projects = projects.filter(book__id=book_id) + if group_projects == "true": + projects = projects.filter(is_group_project=True) + serializer = ProjectSerializer(projects, many=True, context={'request': request}) return Response(serializer.data, status=status.HTTP_200_OK) except Exception as ex: diff --git a/LearningAPI/views/team_maker_view.py b/LearningAPI/views/team_maker_view.py index e29e2c1..809dae1 100644 --- a/LearningAPI/views/team_maker_view.py +++ b/LearningAPI/views/team_maker_view.py @@ -10,15 +10,22 @@ from LearningAPI.utils import GithubRequest, SlackAPI +class TeamRepoSerializer(serializers.ModelSerializer): + class Meta: + model = GroupProjectRepository + fields = ( 'id', 'project', 'repository' ) + + class StudentTeamSerializer(serializers.ModelSerializer): students = serializers.SerializerMethodField() + repositories = TeamRepoSerializer(many=True) def get_students(self, obj): return [{'id': student.id, 'name': student.name} for student in obj.students.all() if not student.user.is_staff] class Meta: model = StudentTeam - fields = ( 'group_name', 'cohort', 'sprint_team', 'students' ) + fields = ( 'group_name', 'cohort', 'sprint_team', 'students', 'repositories' ) class TeamMakerView(ViewSet): @@ -71,10 +78,6 @@ def create(self, request): # Create group project repository if group project is not None if group_project_id is not None: project = Project.objects.get(pk=group_project_id) - group_project_repo = GroupProjectRepository() - group_project_repo.team_id = team.id - group_project_repo.project = project - group_project_repo.save() # Get student Github organization name student_org_name = cohort.info.student_organization_url.split("/")[-1] @@ -104,6 +107,14 @@ def create(self, request): student=student ) + # Save the team's client-side repository URL to the database + group_project_repo = GroupProjectRepository() + group_project_repo.team_id = team.id + group_project_repo.project = project + group_project_repo.repository = f'https://github.com/{student_org_name}/{repo_name}' + group_project_repo.save() + + # Send message to project team's Slack channel with the repository URL created_repo_url = f'https://github.com/{student_org_name}/{repo_name}' slack.send_message( @@ -131,6 +142,13 @@ def create(self, request): student=student ) + # Save the team's API repository URL to the database + group_project_repo = GroupProjectRepository() + group_project_repo.team_id = team.id + group_project_repo.project = project + group_project_repo.repository = f'https://github.com/{student_org_name}/{api_repo_name}' + group_project_repo.save() + # Send message to project team's Slack channel with the repository URL created_repo_url = f'https://github.com/{student_org_name}/{api_repo_name}' slack.send_message( diff --git a/learopsdev.session.sql b/learopsdev.session.sql index c2556a3..844d4ad 100644 --- a/learopsdev.session.sql +++ b/learopsdev.session.sql @@ -8,12 +8,8 @@ SELECT * from "socialaccount_socialaccount" order by id desc; SELECT * from "authtoken_token" where user_id = 470; SELECT * from "LearningAPI_nssuser" where user_id = 470; SELECT * from "LearningAPI_nssusercohort" where nss_user_id = 468; - -DELETE FROM "LearningAPI_cohort"; -DELETE FROM "LearningAPI_nssuserteam"; -DELETE FROM "LearningAPI_groupprojectrepository"; -DELETE FROM "LearningAPI_studentteam"; SELECT * from "LearningAPI_studentteam"; +SELECT * FROM "LearningAPI_cohort"; DROP FUNCTION IF EXISTS get_student_details(INT); DROP FUNCTION IF EXISTS get_project_average_start_delay(INT); From d8de3dc05b090f3313651200d9fed472609e3d1b Mon Sep 17 00:00:00 2001 From: Steve Brownlee Date: Wed, 2 Oct 2024 17:07:55 -0400 Subject: [PATCH 8/9] Deleting Slack channels --- LearningAPI/utils.py | 23 +++++++++++++++++------ LearningAPI/views/team_maker_view.py | 24 +++++++++++------------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/LearningAPI/utils.py b/LearningAPI/utils.py index 9478978..ef2cce9 100644 --- a/LearningAPI/utils.py +++ b/LearningAPI/utils.py @@ -1,8 +1,4 @@ -import json -import time -import os -import logging -import requests +import json, random, string, time, os, logging, requests from requests.exceptions import ConnectionError from LearningAPI.models.people import NssUser @@ -31,9 +27,24 @@ def send_message(self, channel, text): return response.json() + def delete_channel(self, channel_id): + channel_payload = { + "channel": channel_id, + "token": os.getenv("SLACK_BOT_TOKEN") + } + + # Create a Slack channel with the given name + res = requests.post( + "https://slack.com/api/conversations.archive", + timeout=10, + data=channel_payload, + headers=self.headers + ) + channel_res = res.json() + return channel_res['ok'] + def create_channel(self, name, members): """Create a Slack channel for a student team""" - channel_payload = { "name": name, "token": os.getenv("SLACK_BOT_TOKEN") diff --git a/LearningAPI/views/team_maker_view.py b/LearningAPI/views/team_maker_view.py index 809dae1..b22c1e5 100644 --- a/LearningAPI/views/team_maker_view.py +++ b/LearningAPI/views/team_maker_view.py @@ -63,8 +63,10 @@ def create(self, request): team.sprint_team = group_project_id is not None # Create the Slack channel and add students to it and store the channel ID in the team + # The cohort name will always end in a number. Split on the space and get the last item slack = SlackAPI() - channel_name = f"{team_prefix}-{cohort.name.replace(' ', '-').lower()}-{team_index}" + random_team_suffix = ''.join(random.choice(string.ascii_lowercase) for i in range(6)) + channel_name = f"{team_prefix}-{cohort.name.split(' ')[-1]}-{random_team_suffix}" team.slack_channel = slack.create_channel(channel_name, student_list) team.save() @@ -169,27 +171,23 @@ def reset(self, request): try: cohort = Cohort.objects.get(pk=cohort_id) + self._delete_slack_channels(cohort) self._delete_cohort_teams(cohort) return Response(None, status=status.HTTP_204_NO_CONTENT) except Cohort.DoesNotExist as ex: return Response({'message': str(ex)}, status=status.HTTP_404_NOT_FOUND) + except Exception as ex: + return Response({'message': str(ex)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def _delete_cohort_teams(self, cohort): NSSUserTeam.objects.filter(team__cohort=cohort).delete() GroupProjectRepository.objects.filter(team__cohort=cohort).delete() StudentTeam.objects.filter(cohort=cohort).delete() - # def destroy(self, request, pk=None): + def _delete_slack_channels(self, cohort): + current_teams = StudentTeam.objects.filter(cohort=cohort) - # if pk is None: - # return Response({'message': 'No cohort ID provided'}, status=status.HTTP_400_BAD_REQUEST) + for team in current_teams: + slack = SlackAPI() + slack.delete_channel(team.slack_channel) - # try: - # cohort = Cohort.objects.get(pk=pk) - # NSSUserTeam.objects.filter(team__cohort=cohort).delete() - # GroupProjectRepository.objects.filter(team__cohort=cohort).delete() - # StudentTeam.objects.filter(cohort=cohort).delete() - - # return Response(None, status=status.HTTP_204_NO_CONTENT) - # except Cohort.DoesNotExist as ex: - # return Response({'message': str(ex)}, status=status.HTTP_404_NOT_FOUND) From 9229a4ab9ea2bcdaa003b41428bfbfa7b03dde11 Mon Sep 17 00:00:00 2001 From: Steve Brownlee Date: Wed, 9 Oct 2024 12:29:37 -0400 Subject: [PATCH 9/9] Ignore all .env files --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 93944dc..7328249 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,4 @@ prod_backup*.sql setup learning.service logs/ -.env +*.env