Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add date_created Field to Entity and Display in Metadata Component #2909

Closed
wants to merge 12 commits into from
Closed
5 changes: 5 additions & 0 deletions pontoon/base/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ class TranslationAdmin(admin.ModelAdmin):
raw_id_fields = ("entity",)


class ProjectSlugHistoryAdmin(admin.ModelAdmin):
readonly_fields = ("created_at",)


class CommentAdmin(admin.ModelAdmin):
raw_id_fields = ("translation", "entity")

Expand Down Expand Up @@ -367,3 +371,4 @@ def performed_by_email(self, obj):
admin.site.register(models.ChangedEntityLocale, ChangedEntityLocaleAdmin)
admin.site.register(models.PermissionChangelog, UserRoleLogActionAdmin)
admin.site.register(models.Comment, CommentAdmin)
admin.site.register(models.ProjectSlugHistory, ProjectSlugHistoryAdmin)
36 changes: 36 additions & 0 deletions pontoon/base/migrations/0046_projectslughistory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 3.2.15 on 2023-06-21 18:54

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("base", "0045_drop_project_links_url_width"),
]

operations = [
migrations.CreateModel(
name="ProjectSlugHistory",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("old_slug", models.SlugField(max_length=255)),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"project",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="base.project"
),
),
],
),
]
7 changes: 7 additions & 0 deletions pontoon/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,12 @@ def available_locales_list(self):
return list(self.locales.all().values_list("code", flat=True))


class ProjectSlugHistory(models.Model):
project = models.ForeignKey("Project", on_delete=models.CASCADE)
old_slug = models.SlugField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)


class UserProfile(models.Model):
# This field is required.
user = models.OneToOneField(
Expand Down Expand Up @@ -3070,6 +3076,7 @@ def map_entities(
"translation": translation_array,
"readonly": entity.resource.project.projectlocale[0].readonly,
"is_sibling": is_sibling,
"date_created": entity.date_created,
}
)

Expand Down
17 changes: 17 additions & 0 deletions pontoon/base/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
ProjectLocale,
TranslatedResource,
UserProfile,
ProjectSlugHistory,
)


Expand Down Expand Up @@ -206,3 +207,19 @@ def assign_project_locale_group_permissions(sender, **kwargs):
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)


@receiver(pre_save, sender=Project)
def create_slug_history(sender, instance, **kwargs):
"""
Signal receiver that, prior to saving a Project instance, creates a ProjectSlugHistory object if the project's slug has changed.
"""
if instance.pk: # checks if instance is not a new object
try:
old_instance = sender.objects.get(pk=instance.pk)
if old_instance.slug != instance.slug:
ProjectSlugHistory.objects.create(
project=instance, old_slug=old_instance.slug
)
except sender.DoesNotExist:
pass
1 change: 1 addition & 0 deletions pontoon/base/tests/models/test_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ def test_entity_project_locale_no_paths(
"machinery_original": str(entity_a.string),
"readonly": False,
"is_sibling": False,
"date_created": entity_a.date_created,
}
assert entities[0] == expected

Expand Down
271 changes: 271 additions & 0 deletions pontoon/base/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from django.contrib.auth import get_user_model

from django.urls import reverse
from django.urls.exceptions import NoReverseMatch

from pontoon.base.models import Project
from pontoon.base.utils import (
aware_datetime,
Expand All @@ -13,6 +16,274 @@
is_email,
)

from pontoon.test.factories import (
ProjectFactory,
ResourceFactory,
LocaleFactory,
ProjectSlugHistoryFactory,
ProjectLocaleFactory,
)


@pytest.fixture
def project_d():
"""
Fixture that sets up and returns a Project with associated Locale and Resource.
"""
locale = LocaleFactory.create()
project = ProjectFactory.create(
name="Project D", slug="project-d", disabled=False, system_project=False
)
ResourceFactory.create(project=project, path="resource_d.po", format="po")
ProjectLocaleFactory.create(project=project, locale=locale)
return project


def create_slug_history_and_change_slug(project, new_slug):
"""
This function is a helper for tests that need to simulate changing a project's slug.
It records the project's current slug in the history, then updates the project's slug
to a new value.
"""
# Record the old slug in the history
ProjectSlugHistoryFactory.create(project=project, old_slug=project.slug)

# Change the slug of the project to the new_slug
project.slug = new_slug
project.save()
project.refresh_from_db()

return project


@pytest.mark.django_db
def test_project_view_redirects_old_slug(client, project_d):
"""
Test to ensure that accessing a project view with an old slug redirects to the new slug URL.
"""
old_slug = project_d.slug
new_slug = "project-d-new-1"
project_d = create_slug_history_and_change_slug(project_d, new_slug)

# First access the URL with the new slug and ensure it's working
response = client.get(
reverse("pontoon.projects.project", kwargs={"slug": new_slug})
)
assert response.status_code == 200

# Now access the URL with the old slug
response = client.get(
reverse("pontoon.projects.project", kwargs={"slug": old_slug})
)
# The old slug should cause a redirect to the new slug URL
assert response.status_code == 302
assert response.url == reverse(
"pontoon.projects.project", kwargs={"slug": new_slug}
)


@pytest.mark.django_db
def test_handle_old_slug_redirect_no_loop(client, project_d):
"""
Test that there is no redirect loop when a project's slug is renamed from 'cc' to 'dd' and then back to 'cc'.
"""
# Rename project from 'cc' to 'dd' and then back to 'cc'
create_slug_history_and_change_slug(project_d, "cc")
create_slug_history_and_change_slug(project_d, "dd")
create_slug_history_and_change_slug(project_d, "cc")

# Request the project detail view with slug 'cc'
response = client.get(reverse("pontoon.projects.project", kwargs={"slug": "cc"}))

# Assert that the response is not a redirect (status code is not 302)
assert response.status_code != 302


@pytest.mark.django_db
def test_handle_old_slug_redirect_no_redirect_to_different_project(client, project_d):
"""
Test that a request for a slug that was changed and then reused by a different project does not redirect to the original project.
"""
# Rename project from 'ee' to 'ff'
create_slug_history_and_change_slug(project_d, "ee")
create_slug_history_and_change_slug(project_d, "ff")

# Create a new project with slug 'ee'
project = ProjectFactory.create(
name="Project E", slug="ee", disabled=False, system_project=False
)
ResourceFactory.create(project=project, path="resource_e.po", format="po")

# Request the project detail view with slug 'ee'
response = client.get(reverse("pontoon.projects.project", kwargs={"slug": "ee"}))

# Assert that the response is successful (status code is 200)
assert response.status_code == 200


@pytest.mark.django_db
def test_handle_no_slug_redirect_project(client):
"""
Test to ensure that an attempt to access a project view without a slug raises a NoReverseMatch exception.
"""
with pytest.raises(NoReverseMatch):
# Try to access the URL without a slug
client.get(reverse("pontoon.projects.project", kwargs={}))


@pytest.mark.django_db
def test_handle_nonexistent_slug_redirect_project(client):
"""
Test to ensure that an attempt to access a project view with a non-existent slug returns a 404 error.
"""
slug = "nonexistent-slug"

response = client.get(reverse("pontoon.projects.project", kwargs={"slug": slug}))

# The expectation here is that the server should return a 404 error
assert response.status_code == 404


@pytest.mark.django_db
def test_translation_view_redirects_old_slug(client, project_d):
"""
Test to ensure that accessing a translation view with an old slug redirects to the new slug URL.
"""
# Add resource to project
resource_path = "resource_d.po"

old_slug = project_d.slug
new_slug = "project-d-new-2"
locale = project_d.locales.first().code
project_d = create_slug_history_and_change_slug(project_d, new_slug)

# First access the URL with the new slug and ensure it's working
response = client.get(
reverse(
"pontoon.translate",
kwargs={"project": new_slug, "locale": locale, "resource": resource_path},
)
)
assert response.status_code == 200

# Now access the URL with the old slug
response = client.get(
reverse(
"pontoon.translate",
kwargs={"project": old_slug, "locale": locale, "resource": resource_path},
)
)
# The old slug should cause a redirect to the new slug URL
assert response.status_code == 302
assert response.url == reverse(
"pontoon.translate",
kwargs={"project": new_slug, "locale": locale, "resource": resource_path},
)


@pytest.mark.django_db
def test_handle_no_slug_redirect_translate(client, project_d):
"""
Test to ensure that an attempt to access a translate view without a slug raises a NoReverseMatch exception.
"""
locale = project_d.locales.first().code
resource_path = "resource_d.po"

with pytest.raises(NoReverseMatch):
client.get(
reverse(
"pontoon.translate",
kwargs={"locale": locale, "resource": resource_path},
)
)


@pytest.mark.django_db
def test_handle_nonexistent_slug_redirect_translate(client, project_d):
"""
Test to ensure that an attempt to access a translate view with a non-existent slug returns a 404 error.
"""
locale = project_d.locales.first().code
resource_path = "resource_d.po"
slug = "nonexistent-slug"

response = client.get(
reverse(
"pontoon.translate",
kwargs={"project": slug, "locale": locale, "resource": resource_path},
)
)

assert response.status_code == 404


@pytest.mark.django_db
def test_localization_view_redirects_old_slug(client, project_d):
"""
Test to ensure that accessing a localization view with an old slug redirects to the new slug URL.
"""
old_slug = project_d.slug
new_slug = "project-d-new-3"
locale = project_d.locales.first().code
project_d = create_slug_history_and_change_slug(project_d, new_slug)

# First access the URL with the new slug and ensure it's working
response = client.get(
reverse(
"pontoon.localizations.localization",
kwargs={"slug": new_slug, "code": locale},
)
)
assert response.status_code == 200

# Now access the URL with the old slug
response = client.get(
reverse(
"pontoon.localizations.localization",
kwargs={"slug": old_slug, "code": locale},
)
)
# The old slug should cause a redirect to the new slug URL
assert response.status_code == 302
assert response.url == reverse(
"pontoon.localizations.localization",
kwargs={"slug": new_slug, "code": locale},
)


@pytest.mark.django_db
def test_handle_no_slug_redirect_localization(client, project_d):
"""
Test to ensure that an attempt to access a localization view without a slug raises a NoReverseMatch exception.
"""
locale = project_d.locales.first().code

with pytest.raises(NoReverseMatch):
client.get(
reverse(
"pontoon.localizations.localization",
kwargs={"code": locale},
)
)


@pytest.mark.django_db
def test_handle_nonexistent_slug_redirect_localization(client, project_d):
"""
Test to ensure that an attempt to access a localization view with a non-existent slug returns a 404 error.
"""
locale = project_d.locales.first().code
slug = "nonexistent-slug"

response = client.get(
reverse(
"pontoon.localizations.localization",
kwargs={"slug": slug, "code": locale},
)
)

assert response.status_code == 404


@pytest.mark.django_db
def test_get_m2m_changes_no_change(user_a):
Expand Down
Loading