- In Progress {{in_progress.count}} -
- {% for ticket in in_progress %} - -#{{ticket.id}} {{ticket.title}} - {{ticket.team.tla}}
-Assigned to: {{ticket.assignee}} via {{ticket.queue}}
-Last Updated: {{ticket.last_updated}}
-diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1f3555e..d2e89b6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,6 +33,8 @@ jobs: run: cp helpdesk/helpdesk/configuration.dev.py helpdesk/helpdesk/configuration.py - name: Static type checking run: make type + - name: Formatting + run: make format-check - name: Lint run: make lint - name: Unit tests diff --git a/.gitignore b/.gitignore index 1d4cec2..e9ad518 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ helpdesk/helpdesk/configuration.py helpdesk/db.sqlite +helpdesk/static teams.csv ### Django ### diff --git a/Makefile b/Makefile index ea0e220..00b098a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean lint type test test-cov +.PHONY: all clean format format-check lint type test test-cov CMD:= PYMODULE:=helpdesk @@ -6,7 +6,15 @@ MANAGEPY:=$(CMD) ./$(PYMODULE)/manage.py APPS:=helpdesk accounts display teams tickets SPHINX_ARGS:=docs/ docs/_build -nWE -all: type test lint +all: type test format lint + +format: + find $(PYMODULE) -name "*.html" | xargs $(CMD) djhtml + $(CMD) ruff format $(PYMODULE) + +format-check: + find $(PYMODULE) -name "*.html" | xargs $(CMD) djhtml --check + $(CMD) ruff format --check $(PYMODULE) lint: $(CMD) ruff check $(PYMODULE) diff --git a/README.md b/README.md index b767d9a..5e81567 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,17 @@ The `Makefile` contains commands that can be used to run tests and linting: - `make test` - Run unit tests - `make type` - Type checking -## SR2023 Deployment +## Deployment -This system was deployed for the helpdesk at the SR2023 competition as an experiment and received positive feedback. It was deployed on a 512MB 1 core machine on [Fly](https://fly.io) with a separate Postgres database server of the same specifications. \ No newline at end of file +This system is deployed using our [Ansible](https://github.com/srobo/ansible/) configuration. + +### Login with Google + +Credentials are configured through the Django admin. OAuth credentials need to be configured as below: + +- User type: Internal (this ensures it's only SR accounts which can be used) +- Scopes: `.../auth/userinfo.email`, `.../auth/userinfo.profile`, `openid` +- Redirect URIs: `https://studentrobotics.org/helpdesk/auth/google/login/callback/` +- Authorised JavaScript origins: `https://studentrobotics.org` + +A [project](https://console.cloud.google.com/home/dashboard?project=helpdesk-419320) exists for this in our Google Cloud account. diff --git a/helpdesk/accounts/forms.py b/helpdesk/accounts/forms.py index 7eaa3aa..e8b6c13 100644 --- a/helpdesk/accounts/forms.py +++ b/helpdesk/accounts/forms.py @@ -9,7 +9,6 @@ class SignupForm(UserCreationForm): - signup_code = forms.CharField( label="Volunteer Signup Code", help_text="This code verifies that you are a volunteer. It can be found in the break room.", @@ -20,7 +19,7 @@ class SignupForm(UserCreationForm): class Meta: model = User fields = ("username",) - field_classes = {'username': UsernameField} + field_classes = {"username": UsernameField} def clean_signup_code(self) -> None: signup_code = self.cleaned_data.get("signup_code") diff --git a/helpdesk/accounts/middleware.py b/helpdesk/accounts/middleware.py index 0fa630e..da2f4e9 100644 --- a/helpdesk/accounts/middleware.py +++ b/helpdesk/accounts/middleware.py @@ -8,7 +8,6 @@ class ProfileMiddleware: - EXCLUDED_PATHS: set[tuple[str | None, str]] = { (None, "account_logout"), ("accounts", "onboarding"), @@ -19,11 +18,13 @@ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None: def __call__(self, request: HttpRequest) -> HttpResponse: profile_complete = getattr(request.user, "onboarded_at", None) - if all([ - request.user.is_authenticated, - not profile_complete, - self._request_requires_profile(request), - ]): + if all( + [ + request.user.is_authenticated, + not profile_complete, + self._request_requires_profile(request), + ] + ): return redirect("accounts:onboarding") return self.get_response(request) diff --git a/helpdesk/accounts/migrations/0001_create_user_model.py b/helpdesk/accounts/migrations/0001_create_user_model.py index 06ef6d1..5dee94c 100644 --- a/helpdesk/accounts/migrations/0001_create_user_model.py +++ b/helpdesk/accounts/migrations/0001_create_user_model.py @@ -28,7 +28,9 @@ class Migration(migrations.Migration): ( "last_login", models.DateTimeField( - blank=True, null=True, verbose_name="last login", + blank=True, + null=True, + verbose_name="last login", ), ), ( @@ -61,7 +63,9 @@ class Migration(migrations.Migration): ( "email", models.EmailField( - blank=True, max_length=254, verbose_name="email address", + blank=True, + max_length=254, + verbose_name="email address", ), ), ( @@ -83,7 +87,8 @@ class Migration(migrations.Migration): ( "date_joined", models.DateTimeField( - default=django.utils.timezone.now, verbose_name="date joined", + default=django.utils.timezone.now, + verbose_name="date joined", ), ), ], diff --git a/helpdesk/accounts/migrations/0003_auto_20230318_1405.py b/helpdesk/accounts/migrations/0003_auto_20230318_1405.py index af1ebb5..1585895 100644 --- a/helpdesk/accounts/migrations/0003_auto_20230318_1405.py +++ b/helpdesk/accounts/migrations/0003_auto_20230318_1405.py @@ -4,24 +4,23 @@ class Migration(migrations.Migration): - dependencies = [ - ('accounts', '0002_add_default_ticket_queue'), + ("accounts", "0002_add_default_ticket_queue"), ] operations = [ migrations.RemoveField( - model_name='user', - name='name', + model_name="user", + name="name", ), migrations.AddField( - model_name='user', - name='first_name', - field=models.CharField(blank=True, max_length=150, verbose_name='first name'), + model_name="user", + name="first_name", + field=models.CharField(blank=True, max_length=150, verbose_name="first name"), ), migrations.AddField( - model_name='user', - name='last_name', - field=models.CharField(blank=True, max_length=150, verbose_name='last name'), + model_name="user", + name="last_name", + field=models.CharField(blank=True, max_length=150, verbose_name="last name"), ), ] diff --git a/helpdesk/accounts/migrations/0005_user_onboarded_at.py b/helpdesk/accounts/migrations/0005_user_onboarded_at.py index 8c0ffe9..da58b01 100644 --- a/helpdesk/accounts/migrations/0005_user_onboarded_at.py +++ b/helpdesk/accounts/migrations/0005_user_onboarded_at.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('accounts', '0004_allow_default_ticket_queue_to_be_blank'), + ("accounts", "0004_allow_default_ticket_queue_to_be_blank"), ] operations = [ migrations.AddField( - model_name='user', - name='onboarded_at', + model_name="user", + name="onboarded_at", field=models.DateTimeField(blank=True, null=True), ), ] diff --git a/helpdesk/accounts/models.py b/helpdesk/accounts/models.py index cb2db50..9fbde8a 100644 --- a/helpdesk/accounts/models.py +++ b/helpdesk/accounts/models.py @@ -5,7 +5,6 @@ class User(AbstractUser): - # Helpdesk Specific Fields default_ticket_queue = models.ForeignKey( "tickets.TicketQueue", diff --git a/helpdesk/accounts/views.py b/helpdesk/accounts/views.py index 1c12d36..356eb56 100644 --- a/helpdesk/accounts/views.py +++ b/helpdesk/accounts/views.py @@ -17,7 +17,6 @@ class ProfileUpdateView(LoginRequiredMixin, UpdateView): - model = User fields = ["first_name", "last_name", "default_ticket_queue"] @@ -28,8 +27,8 @@ def get_object(self, queryset: models.QuerySet[User] | None = None) -> User: def get_form(self, form_class: type[BaseModelForm] | None = None) -> BaseModelForm: form = super().get_form(form_class) # Modify generated form to require a name. - form.fields['first_name'].required = True - form.fields['first_name'].label = "Given name" + form.fields["first_name"].required = True + form.fields["first_name"].label = "Given name" return form def get_success_url(self) -> str: @@ -38,7 +37,6 @@ def get_success_url(self) -> str: class OnboardingView(LoginRequiredMixin, UpdateView): - model = User fields = ["first_name", "last_name", "default_ticket_queue"] template_name = "accounts/onboarding.html" @@ -57,8 +55,8 @@ def get_object(self, queryset: models.QuerySet[User] | None = None) -> User: def get_form(self, form_class: type[BaseModelForm] | None = None) -> BaseModelForm: form = super().get_form(form_class) # Modify generated form to require a name. - form.fields['first_name'].required = True - form.fields['first_name'].label = "Given name" + form.fields["first_name"].required = True + form.fields["first_name"].label = "Given name" return form def form_valid(self, form: BaseModelForm) -> HttpResponse: @@ -72,5 +70,5 @@ def get_success_url(self) -> str: class SignupView(CreateView): form_class = SignupForm - success_url = reverse_lazy('account_login') - template_name = 'accounts/signup.html' + success_url = reverse_lazy("account_login") + template_name = "accounts/signup.html" diff --git a/helpdesk/display/apps.py b/helpdesk/display/apps.py index c79d703..1f0c36d 100644 --- a/helpdesk/display/apps.py +++ b/helpdesk/display/apps.py @@ -2,5 +2,5 @@ class DisplayConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'display' + default_auto_field = "django.db.models.BigAutoField" + name = "display" diff --git a/helpdesk/display/views.py b/helpdesk/display/views.py index 62be69c..51f49db 100644 --- a/helpdesk/display/views.py +++ b/helpdesk/display/views.py @@ -7,7 +7,6 @@ class HelpdeskDisplayView(TemplateView): - template_name = "display/helpdesk.html" def get_context_data(self, **kwargs: Any) -> dict[str, Any]: diff --git a/helpdesk/helpdesk/account_adapter.py b/helpdesk/helpdesk/account_adapter.py index c98c143..0e86dd3 100644 --- a/helpdesk/helpdesk/account_adapter.py +++ b/helpdesk/helpdesk/account_adapter.py @@ -4,7 +4,6 @@ class AccountAdapter(DefaultAccountAdapter): - def is_open_for_signup(self, request: HttpRequest) -> bool: """ Checks whether or not the site is open for signups. @@ -17,4 +16,3 @@ def is_open_for_signup(self, request: HttpRequest) -> bool: if request.path.rstrip("/") == reverse("account_signup").rstrip("/"): return False return True - diff --git a/helpdesk/helpdesk/forms.py b/helpdesk/helpdesk/forms.py index 1282692..346b98d 100644 --- a/helpdesk/helpdesk/forms.py +++ b/helpdesk/helpdesk/forms.py @@ -2,6 +2,4 @@ class CommentSubmitForm(forms.Form): - comment = forms.CharField(widget=forms.Textarea(attrs={"rows": "5"})) - diff --git a/helpdesk/helpdesk/settings.py b/helpdesk/helpdesk/settings.py index fb69572..952587d 100644 --- a/helpdesk/helpdesk/settings.py +++ b/helpdesk/helpdesk/settings.py @@ -55,9 +55,7 @@ ADMINS = getattr(configuration, "ADMINS", []) BASE_PATH = getattr(configuration, "BASE_PATH", "") if BASE_PATH: - BASE_PATH = ( - BASE_PATH.strip("/") + "/" - ) # Enforce trailing slash only # pragma: nocover + BASE_PATH = BASE_PATH.strip("/") + "/" # Enforce trailing slash only # pragma: nocover DEBUG = getattr(configuration, "DEBUG", False) EMAIL = getattr(configuration, "EMAIL", {}) SYSTEM_TITLE = getattr(configuration, "SYSTEM_TITLE", "Helpdesk") @@ -101,12 +99,12 @@ "crispy_forms", "crispy_bulma", "django_filters", - 'django_tables2', - 'django_tables2_bulma_template', - 'allauth', - 'allauth.account', - 'allauth.socialaccount', - 'allauth.socialaccount.providers.google', + "django_tables2", + "django_tables2_bulma_template", + "allauth", + "allauth.account", + "allauth.socialaccount", + "allauth.socialaccount.providers.google", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -123,12 +121,13 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "allauth.account.middleware.AccountMiddleware", "accounts.middleware.ProfileMiddleware", ] AUTHENTICATION_BACKENDS = [ - 'django.contrib.auth.backends.ModelBackend', - 'allauth.account.auth_backends.AuthenticationBackend', + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", ] CORS_ALLOW_ALL_ORIGINS = True @@ -193,7 +192,7 @@ # Authentication URLs LOGIN_URL = f"/{BASE_PATH}auth/login/" -LOGOUT_REDIRECT_URL = "/" +LOGOUT_REDIRECT_URL = LOGIN_URL # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field @@ -209,16 +208,16 @@ # Django AllAuth -ACCOUNT_ADAPTER = 'helpdesk.account_adapter.AccountAdapter' -ACCOUNT_DEFAULT_HTTP_PROTOCOL = 'https' +ACCOUNT_ADAPTER = "helpdesk.account_adapter.AccountAdapter" +ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" SOCIALACCOUNT_PROVIDERS = { - 'google': { - 'SCOPE': [ - 'profile', - 'email', + "google": { + "SCOPE": [ + "profile", + "email", ], - 'AUTH_PARAMS': { - 'access_type': 'online', + "AUTH_PARAMS": { + "access_type": "online", }, }, } @@ -227,3 +226,34 @@ SRCOMP_HTTP_BASE_URL = getattr(configuration, "SRCOMP_HTTP_BASE_URL", None) VOLUNTEER_SIGNUP_CODE = getattr(configuration, "VOLUNTEER_SIGNUP_CODE") + + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + # Send logs with at least INFO level to the console. + "console": { + "level": "INFO", + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + }, + "formatters": { + "verbose": { + "format": "[%(asctime)s][%(process)d][%(levelname)s][%(name)s] %(message)s", + }, + }, + "loggers": { + "django.request": { + "handlers": ["console"], + "level": "ERROR", + "propagate": False, + }, + "django.security": { + "handlers": ["console"], + "level": "WARNING", + "propagate": False, + }, + }, +} diff --git a/helpdesk/helpdesk/tables.py b/helpdesk/helpdesk/tables.py index 0662c3b..7ffc80a 100644 --- a/helpdesk/helpdesk/tables.py +++ b/helpdesk/helpdesk/tables.py @@ -2,7 +2,6 @@ class SearchTable(tables.Table): - result_type = tables.Column("Result Type") title = tables.URLColumn(accessor="url", text=lambda x: x["title"]) url = tables.URLColumn(verbose_name="Actions", text="View") diff --git a/helpdesk/helpdesk/tests/test_smoke.py b/helpdesk/helpdesk/tests/test_smoke.py index d53ef63..81531d3 100644 --- a/helpdesk/helpdesk/tests/test_smoke.py +++ b/helpdesk/helpdesk/tests/test_smoke.py @@ -11,11 +11,11 @@ def test_authentication_required(client: Client) -> None: resp = client.get("/") assert resp.status_code == 302 - assert resp['Location'] == '/auth/login/?next=/' + assert resp["Location"] == "/auth/login/?next=/" def test_landing_page_redirects(client: Client, admin_user: User) -> None: client.force_login(admin_user) resp = client.get("/") assert resp.status_code == 302 - assert resp['Location'] == '/accounts/onboarding/' + assert resp["Location"] == "/accounts/onboarding/" diff --git a/helpdesk/helpdesk/urls.py b/helpdesk/helpdesk/urls.py index ed0baa1..25de4b4 100644 --- a/helpdesk/helpdesk/urls.py +++ b/helpdesk/helpdesk/urls.py @@ -13,6 +13,7 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.conf import settings from django.contrib import admin from django.urls import include, path @@ -23,7 +24,7 @@ path(f"{settings.BASE_PATH}", DefaultHomeView.as_view(), name="home"), path(f"{settings.BASE_PATH}search/", SearchView.as_view(), name="search"), path(f"{settings.BASE_PATH}admin/", admin.site.urls), - path(f'{settings.BASE_PATH}auth/', include('allauth.urls')), + path(f"{settings.BASE_PATH}auth/", include("allauth.urls")), path(f"{settings.BASE_PATH}accounts/", include("accounts.urls", namespace="accounts")), path(f"{settings.BASE_PATH}display/", include("display.urls", namespace="display")), path(f"{settings.BASE_PATH}teams/", include("teams.urls", namespace="teams")), diff --git a/helpdesk/helpdesk/utils.py b/helpdesk/helpdesk/utils.py index 75c8c21..c148eb0 100644 --- a/helpdesk/helpdesk/utils.py +++ b/helpdesk/helpdesk/utils.py @@ -6,6 +6,7 @@ ModelT = TypeVar("ModelT", bound=Model) + def get_object_or_none(model: type[ModelT], **kwargs: Any) -> ModelT | None: try: return model.objects.get(**kwargs) # type: ignore[attr-defined] diff --git a/helpdesk/helpdesk/views.py b/helpdesk/helpdesk/views.py index e9f7197..f197c9c 100644 --- a/helpdesk/helpdesk/views.py +++ b/helpdesk/helpdesk/views.py @@ -22,7 +22,8 @@ def get_redirect_url(self, *arg: Any, **kwargs: Any) -> str | None: # Redirect the user to a default queue if they have one if ticket_queue := self.request.user.default_ticket_queue: return reverse_lazy( - "tickets:queue_detail", kwargs={"slug": ticket_queue.slug}, + "tickets:queue_detail", + kwargs={"slug": ticket_queue.slug}, ) else: return reverse_lazy("teams:team_list") @@ -44,19 +45,18 @@ def get_redirect_url(self, *arg: Any, **kwargs: Any) -> str | None: return reverse_lazy("teams:team_list") return reverse_lazy( - "tickets:queue_detail", kwargs={"slug": ticket_queue.slug}, + "tickets:queue_detail", + kwargs={"slug": ticket_queue.slug}, ) class SearchResult(TypedDict): - result_type: Literal["team"] | Literal["ticket"] title: str url: str class SearchView(LoginRequiredMixin, SingleTableMixin, TemplateView): - template_name = "search.html" table_class = SearchTable @@ -77,20 +77,17 @@ def _get_filters(self, q: str) -> dict[type[Ticket] | type[Team], Q]: def get_result_count(self, q: str) -> int: filters = self._get_filters(q) return sum( - [ - model.objects.filter(q_filter).distinct().count() - for model, q_filter in filters.items() - ], + [model.objects.filter(q_filter).distinct().count() for model, q_filter in filters.items()], ) def get_results(self, q: str) -> Generator[SearchResult, None, None]: filters = self._get_filters(q) for team in Team.objects.filter(filters[Team]): - yield SearchResult(result_type='team', title=team.name, url=team.get_absolute_url()) + yield SearchResult(result_type="team", title=team.name, url=team.get_absolute_url()) for ticket in Ticket.objects.filter(filters[Ticket]).distinct(): - yield SearchResult(result_type='ticket', title=ticket.title, url=ticket.get_absolute_url()) + yield SearchResult(result_type="ticket", title=ticket.title, url=ticket.get_absolute_url()) def get_table_data(self) -> Generator[SearchResult, None, None]: q = self._get_query() diff --git a/helpdesk/manage.py b/helpdesk/manage.py index 795763a..4147fbb 100755 --- a/helpdesk/manage.py +++ b/helpdesk/manage.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys diff --git a/helpdesk/pyproject.toml b/helpdesk/pyproject.toml index 85b3ead..fd3d8ba 100644 --- a/helpdesk/pyproject.toml +++ b/helpdesk/pyproject.toml @@ -22,7 +22,10 @@ module = ["django_tables2.*", "allauth.*"] ignore_missing_imports = true [tool.ruff] -select = [ +line-length = 120 + +[tool.ruff.lint] +extend-select = [ "A", "ANN", "B", @@ -41,12 +44,11 @@ select = [ "UP", "W", ] -ignore = [ +extend-ignore = [ "ANN101", # Missing type annotation for `self` in method "ANN401", # Dynamically typed expressions (typing.Any) are disallowed "B009", # Do not call `getattr` with a constant attribute value. + "COM812", # Trailing comma missing, conflicts with black styling "S101", # S101 Use of `assert` detected "N999", # N999 Invalid module name ] - -line-length = 120 diff --git a/helpdesk/teams/admin.py b/helpdesk/teams/admin.py index 4bd4d47..ca44666 100644 --- a/helpdesk/teams/admin.py +++ b/helpdesk/teams/admin.py @@ -4,18 +4,20 @@ class TeamPitLocationAdmin(admin.ModelAdmin): - list_display = ("name", ) + list_display = ("name",) + class TeamCommentAdmin(admin.StackedInline): model = TeamComment extra = 1 - readonly_fields = ('created_at', ) + readonly_fields = ("created_at",) + class TeamAdmin(admin.ModelAdmin): list_display = ("tla", "name", "is_rookie") list_filter = ("is_rookie",) - inlines = (TeamCommentAdmin, ) + inlines = (TeamCommentAdmin,) admin.site.register(TeamPitLocation, TeamPitLocationAdmin) diff --git a/helpdesk/teams/filters.py b/helpdesk/teams/filters.py index b02f9b7..ffce2e9 100644 --- a/helpdesk/teams/filters.py +++ b/helpdesk/teams/filters.py @@ -4,7 +4,6 @@ class TeamFilterset(FilterSet): - is_rookie = filters.BooleanFilter() pit_location = filters.ModelChoiceFilter(queryset=TeamPitLocation.objects.all()) diff --git a/helpdesk/teams/management/commands/import_from_srcomp.py b/helpdesk/teams/management/commands/import_from_srcomp.py index ea8230f..a227548 100644 --- a/helpdesk/teams/management/commands/import_from_srcomp.py +++ b/helpdesk/teams/management/commands/import_from_srcomp.py @@ -5,6 +5,7 @@ DEFAULT_SRCOMP = "https://srcomp.studentrobotics.org/comp-api" + class Command(BaseCommand): help = "Import teams and pit locations from SRComp" # noqa: A003 diff --git a/helpdesk/teams/migrations/0001_create_team_model.py b/helpdesk/teams/migrations/0001_create_team_model.py index a1bcf1d..a74cc91 100644 --- a/helpdesk/teams/migrations/0001_create_team_model.py +++ b/helpdesk/teams/migrations/0001_create_team_model.py @@ -29,7 +29,8 @@ class Migration(migrations.Migration): unique=True, validators=[ django.core.validators.RegexValidator( - "^[A-Z]{3}\\d*$", "Must match TLA format.", + "^[A-Z]{3}\\d*$", + "Must match TLA format.", ), ], verbose_name="TLA", diff --git a/helpdesk/teams/migrations/0002_add_team_pit_locations.py b/helpdesk/teams/migrations/0002_add_team_pit_locations.py index e152dfb..c1e1ad2 100644 --- a/helpdesk/teams/migrations/0002_add_team_pit_locations.py +++ b/helpdesk/teams/migrations/0002_add_team_pit_locations.py @@ -5,24 +5,23 @@ class Migration(migrations.Migration): - dependencies = [ - ('teams', '0001_create_team_model'), + ("teams", "0001_create_team_model"), ] operations = [ migrations.CreateModel( - name='TeamPitLocation', + name="TeamPitLocation", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, verbose_name='Name')), - ('slug', models.CharField(max_length=30, verbose_name='Slug')), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=100, verbose_name="Name")), + ("slug", models.CharField(max_length=30, verbose_name="Slug")), ], ), migrations.AddField( - model_name='team', - name='pit_location', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to='teams.teampitlocation'), + model_name="team", + name="pit_location", + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to="teams.teampitlocation"), preserve_default=False, ), ] diff --git a/helpdesk/teams/migrations/0003_add_missing_unique_constraint.py b/helpdesk/teams/migrations/0003_add_missing_unique_constraint.py index 81b5e99..a0cd162 100644 --- a/helpdesk/teams/migrations/0003_add_missing_unique_constraint.py +++ b/helpdesk/teams/migrations/0003_add_missing_unique_constraint.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('teams', '0002_add_team_pit_locations'), + ("teams", "0002_add_team_pit_locations"), ] operations = [ migrations.AlterField( - model_name='teampitlocation', - name='slug', - field=models.CharField(max_length=30, unique=True, verbose_name='Slug'), + model_name="teampitlocation", + name="slug", + field=models.CharField(max_length=30, unique=True, verbose_name="Slug"), ), ] diff --git a/helpdesk/teams/models.py b/helpdesk/teams/models.py index 669fbdb..ec78998 100644 --- a/helpdesk/teams/models.py +++ b/helpdesk/teams/models.py @@ -6,7 +6,6 @@ class TeamPitLocation(models.Model): - name = models.CharField("Name", max_length=100) slug = models.CharField("Slug", max_length=30, unique=True) @@ -34,7 +33,7 @@ def __str__(self) -> str: return f"{self.tla} - {self.name}" def get_absolute_url(self) -> str: - return reverse_lazy('teams:team_detail', args=[self.tla]) + return reverse_lazy("teams:team_detail", args=[self.tla]) class TeamComment(models.Model): diff --git a/helpdesk/teams/srcomp.py b/helpdesk/teams/srcomp.py index b9e7553..10935b7 100644 --- a/helpdesk/teams/srcomp.py +++ b/helpdesk/teams/srcomp.py @@ -1,4 +1,5 @@ """A basic client for SRComp HTTP.""" + from __future__ import annotations from json import JSONDecodeError @@ -10,14 +11,12 @@ class ScoreInfo(NamedTuple): - league_pos: int game_score: int league_score: int class SRComp: - def __init__(self, *, base_url: str | None = None) -> None: self._base_url = base_url or settings.SRCOMP_HTTP_BASE_URL @@ -48,4 +47,5 @@ def get_score_info_for_team(self, tla: str) -> ScoreInfo | None: ) return None + srcomp = SRComp() diff --git a/helpdesk/teams/tables.py b/helpdesk/teams/tables.py index a4103eb..fd43bca 100644 --- a/helpdesk/teams/tables.py +++ b/helpdesk/teams/tables.py @@ -4,11 +4,10 @@ class TeamTable(tables.Table): - tla = tables.Column() - name = tables.LinkColumn('teams:team_detail', args=[tables.A('tla')]) + name = tables.LinkColumn("teams:team_detail", args=[tables.A("tla")]) is_rookie = tables.BooleanColumn() - actions = tables.LinkColumn('teams:team_detail', args=[tables.A('tla')], text="View") + actions = tables.LinkColumn("teams:team_detail", args=[tables.A("tla")], text="View") class Meta: model = Team diff --git a/helpdesk/teams/views.py b/helpdesk/teams/views.py index 3faf326..17ab927 100644 --- a/helpdesk/teams/views.py +++ b/helpdesk/teams/views.py @@ -24,7 +24,6 @@ class TicketDetailRedirectView(RedirectView): - pattern_name = "teams:team_detail_tickets" @@ -33,8 +32,8 @@ class TeamListView(LoginRequiredMixin, SingleTableMixin, FilterView): table_class = TeamTable filterset_class = TeamFilterset -class TeamDetailAboutView(LoginRequiredMixin, DetailView): +class TeamDetailAboutView(LoginRequiredMixin, DetailView): model = Team slug_field = "tla" template_name_suffix = "_detail_about" @@ -43,8 +42,8 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: score_info = srcomp.get_score_info_for_team(self.object.tla) return super().get_context_data(score_info=score_info, **kwargs) -class TeamDetailCommentsView(LoginRequiredMixin, DetailView): +class TeamDetailCommentsView(LoginRequiredMixin, DetailView): model = Team slug_field = "tla" template_name_suffix = "_detail_comments" @@ -55,21 +54,21 @@ def get_context_data(self, **kwargs: dict[str, Any]) -> dict[str, Any]: **kwargs, ) -class TeamSubmitCommentFormView(LoginRequiredMixin, FormMixin, SingleObjectMixin, ProcessFormView): - http_method_names = ['post', 'put'] +class TeamSubmitCommentFormView(LoginRequiredMixin, FormMixin, SingleObjectMixin, ProcessFormView): + http_method_names = ["post", "put"] model = Team slug_field = "tla" form_class = CommentSubmitForm def get_success_url(self) -> str: - return reverse_lazy('teams:team_detail_comments', kwargs={"slug": self.get_object().tla}) + return reverse_lazy("teams:team_detail_comments", kwargs={"slug": self.get_object().tla}) def form_valid(self, form: CommentSubmitForm) -> HttpResponse: assert self.request.user.is_authenticated team = self.get_object() team.comments.create( - content=form.cleaned_data['comment'], + content=form.cleaned_data["comment"], author=self.request.user, ) return HttpResponseRedirect(redirect_to=self.get_success_url()) @@ -107,7 +106,6 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: class TeamDetailTimelineView(LoginRequiredMixin, DetailView): - model = Team slug_field = "tla" template_name_suffix = "_detail_timeline" @@ -115,37 +113,54 @@ class TeamDetailTimelineView(LoginRequiredMixin, DetailView): def get_entries(self) -> QuerySet[Any]: fields = ("entry_type", "entry_timestamp", "entry_user", "entry_content", "entry_style_info") - ticket_opens = TicketEvent.objects.filter(ticket__team=self.object, new_status__exact="OP").annotate( - entry_type=Value('Ticket Opened ', output_field=CharField()), - entry_timestamp=F("created_at"), - entry_user=F("user"), - entry_content=F("comment"), - entry_style_info=Value('is-info', output_field=CharField()), - ).values(*fields) - - ticket_resolves = TicketEvent.objects.filter(ticket__team=self.object, new_status__exact="RS").annotate( - entry_type=Value('Ticket Resolved ', output_field=CharField()), - entry_timestamp=F("created_at"), - entry_user=F("user"), - entry_content=F("comment"), - entry_style_info=Value('is-success', output_field=CharField()), - ).values(*fields) - - ticket_comments = TicketEvent.objects.filter(ticket__team=self.object, new_status__exact="").annotate( - entry_type=Value('Ticket Comment ', output_field=CharField()), - entry_timestamp=F("created_at"), - entry_user=F("user"), - entry_content=F("comment"), - entry_style_info=Value('', output_field=CharField()), - ).values(*fields) - - team_comments = TeamComment.objects.filter(team=self.object).order_by().annotate( - entry_type=Value('Team Comment', output_field=CharField()), - entry_timestamp=F("created_at"), - entry_user=F("author"), - entry_content=F("content"), - entry_style_info=Value('', output_field=CharField()), - ).values(*fields) + ticket_opens = ( + TicketEvent.objects.filter(ticket__team=self.object, new_status__exact="OP") + .annotate( + entry_type=Value("Ticket Opened ", output_field=CharField()), + entry_timestamp=F("created_at"), + entry_user=F("user"), + entry_content=F("comment"), + entry_style_info=Value("is-info", output_field=CharField()), + ) + .values(*fields) + ) + + ticket_resolves = ( + TicketEvent.objects.filter(ticket__team=self.object, new_status__exact="RS") + .annotate( + entry_type=Value("Ticket Resolved ", output_field=CharField()), + entry_timestamp=F("created_at"), + entry_user=F("user"), + entry_content=F("comment"), + entry_style_info=Value("is-success", output_field=CharField()), + ) + .values(*fields) + ) + + ticket_comments = ( + TicketEvent.objects.filter(ticket__team=self.object, new_status__exact="") + .annotate( + entry_type=Value("Ticket Comment ", output_field=CharField()), + entry_timestamp=F("created_at"), + entry_user=F("user"), + entry_content=F("comment"), + entry_style_info=Value("", output_field=CharField()), + ) + .values(*fields) + ) + + team_comments = ( + TeamComment.objects.filter(team=self.object) + .order_by() + .annotate( + entry_type=Value("Team Comment", output_field=CharField()), + entry_timestamp=F("created_at"), + entry_user=F("author"), + entry_content=F("content"), + entry_style_info=Value("", output_field=CharField()), + ) + .values(*fields) + ) return ticket_comments.union(ticket_opens, ticket_resolves, team_comments).order_by("-entry_timestamp") diff --git a/helpdesk/templates/account/login.html b/helpdesk/templates/account/login.html index b1579a3..e28128d 100644 --- a/helpdesk/templates/account/login.html +++ b/helpdesk/templates/account/login.html @@ -6,22 +6,26 @@ {% block title %}Log In{% endblock %} {% block content %} + {% get_providers as socialaccount_providers %} +
{% trans 'Are you sure you want to sign out?' %}
-{% trans 'Are you sure you want to sign out?' %}
+Hi! 👋
-Let's get started by filling out some details. You can always update your information later in your profile settings.
-Hi! 👋
+Let's get started by filling out some details. You can always update your information later in your profile settings.
+- In Progress {{in_progress.count}} -
- {% for ticket in in_progress %} - -#{{ticket.id}} {{ticket.title}} - {{ticket.team.tla}}
-Assigned to: {{ticket.assignee}} via {{ticket.queue}}
-Last Updated: {{ticket.last_updated}}
-- Unassigned {{unassigned.count}} -
- {% for ticket in unassigned %} - -#{{ticket.id}} {{ticket.title}} - {{ticket.team.tla}}
-Waiting in {{ticket.queue}}
-Last Updated: {{ticket.last_updated}}
-+ In Progress {{in_progress.count}} +
+ {% for ticket in in_progress %} + +#{{ticket.id}} {{ticket.title}} - {{ticket.team.tla}}
+Assigned to: {{ticket.assignee}} via {{ticket.queue}}
+Last Updated: {{ticket.last_updated}}
++ Unassigned {{unassigned.count}} +
+ {% for ticket in unassigned %} + +#{{ticket.id}} {{ticket.title}} - {{ticket.team.tla}}
+Waiting in {{ticket.queue}}
+Last Updated: {{ticket.last_updated}}
+Non-SR Google accounts will not work. If you do not have an SR Google Account, please go back and sign up.
+Non-SR Google accounts will not work. If you do not have an SR Google Account, please go back and sign up.