diff --git a/company/templatetags/custom_filters.py b/company/templatetags/custom_filters.py index 31a68a5f8..02386625d 100644 --- a/company/templatetags/custom_filters.py +++ b/company/templatetags/custom_filters.py @@ -7,3 +7,8 @@ def get_item(dictionary, key): """Return the value for `key` in `dictionary`.""" return dictionary.get(key) + + +@register.filter +def before_dot(value): + return str(value).split(".")[0] diff --git a/company/views.py b/company/views.py index 50be80b48..8d1257cfb 100644 --- a/company/views.py +++ b/company/views.py @@ -123,16 +123,12 @@ def post(self, request, *args, **kwargs): return redirect("/accounts/login/") user_domain = get_email_domain(user.email) - company_name = data.get("company_name", "").strip().lower() + company_name = data.get("company_name", "") if user_domain in restricted_domain: messages.error(request, "Login with company email in order to create the company.") return redirect("/") - if user_domain != company_name: - messages.error(request, "Company name doesn't match your email domain.") - return redirect("register_company") - if Company.objects.filter(name=company_name).exists(): messages.error(request, "Company already exists.") return redirect("register_company") @@ -516,7 +512,7 @@ def post(self, request, id, *args, **kwargs): response = requests.get(safe_url, timeout=5) if response.status_code != 200: raise Exception - except requests.exceptions.RequestException: + except requests.exceptions.RequestException as e: messages.error(request, "Domain does not exist.") return redirect("add_domain", id=id) except ValueError: diff --git a/website/api/views.py b/website/api/views.py index acb11b7b1..e0dc2bbff 100644 --- a/website/api/views.py +++ b/website/api/views.py @@ -1,6 +1,7 @@ import json import uuid from datetime import datetime +from urllib.parse import urlparse from django.conf import settings from django.contrib.sites.shortcuts import get_current_site @@ -846,8 +847,26 @@ class TimeLogViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] def perform_create(self, serializer): + organization_url = self.request.data.get("organization_url") + try: - serializer.save(user=self.request.user) + if organization_url: + parsed_url = urlparse(organization_url) + normalized_url = parsed_url.netloc + parsed_url.path + + # Normalize the URL in the Company model (remove the protocol if present) + try: + organization = Company.objects.get( + Q(url__iexact=normalized_url) + | Q(url__iexact=f"http://{normalized_url}") + | Q(url__iexact=f"https://{normalized_url}") + ) + except Company.DoesNotExist: + raise ParseError(detail="Organization not found for the given URL.") + + # Save the TimeLog with the user and organization (if found, or None) + serializer.save(user=self.request.user, organization=organization) + except ValidationError as e: raise ParseError(detail=str(e)) except Exception as e: diff --git a/website/migrations/0154_timelog_organization.py b/website/migrations/0154_timelog_organization.py new file mode 100644 index 000000000..b297524d4 --- /dev/null +++ b/website/migrations/0154_timelog_organization.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.3 on 2024-11-21 13:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0153_delete_contributorstats"), + ] + + operations = [ + migrations.AddField( + model_name="timelog", + name="organization", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="organization", + to="website.company", + ), + ), + ] diff --git a/website/migrations/0155_alter_company_url.py b/website/migrations/0155_alter_company_url.py new file mode 100644 index 000000000..729f128ca --- /dev/null +++ b/website/migrations/0155_alter_company_url.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.3 on 2024-11-22 11:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0154_timelog_organization"), + ] + + operations = [ + migrations.AlterField( + model_name="company", + name="url", + field=models.URLField(unique=True), + ), + ] diff --git a/website/migrations/0156_merge_20241124_0329.py b/website/migrations/0156_merge_20241124_0329.py new file mode 100644 index 000000000..b35b04915 --- /dev/null +++ b/website/migrations/0156_merge_20241124_0329.py @@ -0,0 +1,12 @@ +# Generated by Django 5.1.3 on 2024-11-24 03:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0154_contributors_to_users_20241123_1836"), + ("website", "0155_alter_company_url"), + ] + + operations = [] diff --git a/website/migrations/0157_merge_20241124_0808.py b/website/migrations/0157_merge_20241124_0808.py new file mode 100644 index 000000000..22a501971 --- /dev/null +++ b/website/migrations/0157_merge_20241124_0808.py @@ -0,0 +1,12 @@ +# Generated by Django 5.1.3 on 2024-11-24 08:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0155_merge_20241124_0242"), + ("website", "0156_merge_20241124_0329"), + ] + + operations = [] diff --git a/website/migrations/0158_merge_20241125_0802.py b/website/migrations/0158_merge_20241125_0802.py new file mode 100644 index 000000000..2f811357a --- /dev/null +++ b/website/migrations/0158_merge_20241125_0802.py @@ -0,0 +1,12 @@ +# Generated by Django 5.1.3 on 2024-11-25 08:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0156_merge_20241124_0722"), + ("website", "0157_merge_20241124_0808"), + ] + + operations = [] diff --git a/website/models.py b/website/models.py index a32467e17..0c9e58087 100644 --- a/website/models.py +++ b/website/models.py @@ -68,7 +68,7 @@ class Company(models.Model): name = models.CharField(max_length=255) description = models.CharField(max_length=500, null=True, blank=True) logo = models.ImageField(upload_to="company_logos", null=True, blank=True) - url = models.URLField() + url = models.URLField(unique=True) email = models.EmailField(null=True, blank=True) twitter = models.CharField(max_length=30, null=True, blank=True) facebook = models.URLField(null=True, blank=True) @@ -876,6 +876,10 @@ class TimeLog(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="timelogs" ) + # associate organization with sizzle + organization = models.ForeignKey( + Company, on_delete=models.CASCADE, related_name="organization", null=True, blank=True + ) start_time = models.DateTimeField() end_time = models.DateTimeField(null=True, blank=True) duration = models.DurationField(null=True, blank=True) diff --git a/website/serializers.py b/website/serializers.py index a73857674..32042a100 100644 --- a/website/serializers.py +++ b/website/serializers.py @@ -149,7 +149,16 @@ class Meta: class TimeLogSerializer(serializers.ModelSerializer): class Meta: model = TimeLog - fields = ["id", "user", "start_time", "end_time", "duration", "github_issue_url", "created"] + fields = [ + "id", + "user", + "organization", + "start_time", + "end_time", + "duration", + "github_issue_url", + "created", + ] read_only_fields = [ "id", "user", diff --git a/website/templates/sizzle/time_logs.html b/website/templates/sizzle/time_logs.html index e73278d90..50ece086d 100644 --- a/website/templates/sizzle/time_logs.html +++ b/website/templates/sizzle/time_logs.html @@ -1,7 +1,9 @@ {% extends "base.html" %} +{% load custom_filters %} {% load static %} {% block content %} +
@@ -22,7 +25,7 @@
{% csrf_token %}
- +
Please provide a valid GitHub Issue URL.
+
+ + + Search for organization +
    +
+
{% endif %} @@ -50,6 +69,13 @@

Active Time Log

target="_blank" rel="noopener noreferrer">{{ active_time_log.github_issue_url }}

+

+ Organization: + {{ organization_url }} +

Elapsed Time: 00:00:00

@@ -67,6 +93,7 @@

Existing Time Logs

End Time Duration GitHub Issue URL + Organization @@ -75,11 +102,23 @@

Existing Time Logs

{{ log.start_time|date:"DATETIME_FORMAT" }} {{ log.end_time|date:"DATETIME_FORMAT" }} - {{ log.duration }} + {% comment %} Only show hours, mins, seconds {% endcomment %} + {{ log.duration|before_dot }} {{ log.github_issue_url }} + title="{{ log.github_issue_url }}" + rel="noopener noreferrer">{{ log.github_issue_url | slice:"19:"| truncatechars:30 }} + + + {% if log.organization %} + {{ log.organization }} + {% else %} + - + {% endif %} {% endif %} @@ -124,6 +163,50 @@

Existing Time Logs

let timeLogId = null; let timerInterval = null; + + const organizations_list = {{organizations_list | safe}} + console.log(organizations_list) + + // Handle organization search + $("#organization_search_input").on("input", function () { + const searchTerm = $(this).val().trim().toLowerCase(); + let searchResults = []; + + if (searchTerm !== "") { + searchResults = organizations_list.filter(function (item) { + return item.name.toLowerCase().includes(searchTerm); + }); + } + + // Update the search results display + $("#search_results").empty(); + if (searchResults.length > 0) { + $.each(searchResults, function (index, result) { + const listItem = $("
  • ") + .text(result.name) + .on("click", function () { + // Set the clicked item as the search box value + $("#organization_search_input").val(result.url); + $("#search_results").html(`Selected organization: ${result.name} 🔎`); + }); + + const urlTag = $("").text(result.url); + + listItem.append(urlTag); + + // Add the list item to the search results container + $("#search_results").append(listItem); + }); + } else { + if (searchTerm !== "") { + $("#search_results").text("No results found"); + } else { + $("#search_results").empty(); + } + } + }); + + // Initialize active time log if exists {% if active_time_log %} timeLogId = {{ active_time_log.id }}; @@ -164,16 +247,30 @@

    Existing Time Logs

    $('#start-time-log-form').on('submit', function(event){ event.preventDefault(); const githubIssueUrl = $('#github_issue_url').val().trim(); - - // Simple URL validation - const urlPattern = /^https?:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/\d+$/; - if (!urlPattern.test(githubIssueUrl)) { + const organizationURL = $('#organization_search_input').val().trim(); + + // Simple URL validation for github and organization + const githubUrlPattern = /^https?:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/\d+$/; + if (!githubUrlPattern.test(githubIssueUrl)) { // Display an alert message $('#message-container').html(``); return; } + //only check organizationURL if it is entered + const organizationUrlPattern = /^(https?:\/\/)?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + if (organizationURL &&!organizationUrlPattern.test(organizationURL)) { + // Display an alert message + $('#message-container').html(``); + return; + } + let ajaxdata={ + 'github_issue_url': githubIssueUrl, + 'organization_url': organizationURL + } $.ajax({ url: apiBaseUrl + 'start/', @@ -182,7 +279,7 @@

    Existing Time Logs

    'Authorization': `Token ${token}`, 'Content-Type': 'application/json' }, - data: JSON.stringify({ 'github_issue_url': githubIssueUrl }), + data: JSON.stringify(ajaxdata), success: function(data){ location.reload(); }, diff --git a/website/views/organization.py b/website/views/organization.py index c11996f73..a4e793fd8 100644 --- a/website/views/organization.py +++ b/website/views/organization.py @@ -947,12 +947,24 @@ def sizzle_daily_log(request): def TimeLogListView(request): time_logs = TimeLog.objects.filter(user=request.user).order_by("-start_time") active_time_log = time_logs.filter(end_time__isnull=True).first() + # print the all details of the active time log token, created = Token.objects.get_or_create(user=request.user) + organizations_list_queryset = Company.objects.all().values("url", "name") + organizations_list = list(organizations_list_queryset) + organization_url = None + if active_time_log: + organization_url = active_time_log.organization.url return render( request, "sizzle/time_logs.html", - {"time_logs": time_logs, "active_time_log": active_time_log, "token": token.key}, + { + "time_logs": time_logs, + "active_time_log": active_time_log, + "token": token.key, + "organizations_list": organizations_list, + "organization_url": organization_url, + }, )