Skip to content

Commit

Permalink
urls: Generic object path parsing
Browse files Browse the repository at this point in the history
Avoid using fixed structure for object URLs. All objects are now
referected as path with possibly variable number of elements. This
allows to introduce component categorization in the future, see #263.

This change also forced change of some URLs which would no longer be
distinguishable (anything under component can no longer have variable
suffix).

TODO:
Changed widgets URLs to better fit in the new structure. Redirects for
old ones are in place.

TODO:
Changed object filtering for changes.
- last_changes_url should have just path
- parsing should use path style parsing
  • Loading branch information
nijel committed Aug 8, 2023
1 parent b1a8387 commit 633f704
Show file tree
Hide file tree
Showing 96 changed files with 1,159 additions and 1,647 deletions.
8 changes: 5 additions & 3 deletions weblate/accounts/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ def test_watch(self):
self.assertEqual(self.user.subscription_set.count(), 9)

# Watch project
self.client.post(reverse("watch", kwargs=self.kw_project))
self.client.post(reverse("watch", kwargs={"path": self.project.get_url_path()}))
self.assertEqual(self.user.profile.watched.count(), 1)
self.assertEqual(
self.user.subscription_set.filter(project=self.project).count(), 0
Expand All @@ -492,13 +492,15 @@ def test_watch(self):
)

# Mute notifications for project
self.client.post(reverse("mute", kwargs=self.kw_project))
self.client.post(reverse("mute", kwargs={"path": self.project.get_url_path()}))
self.assertEqual(
self.user.subscription_set.filter(project=self.project).count(), 18
)

# Unwatch project
self.client.post(reverse("unwatch", kwargs=self.kw_project))
self.client.post(
reverse("unwatch", kwargs={"path": self.project.get_url_path()})
)
self.assertEqual(self.user.profile.watched.count(), 0)
self.assertEqual(
self.user.subscription_set.filter(project=self.project).count(), 0
Expand Down
16 changes: 3 additions & 13 deletions weblate/accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,9 @@
path("userdata/", weblate.accounts.views.userdata, name="userdata"),
path("unsubscribe/", weblate.accounts.views.unsubscribe, name="unsubscribe"),
path("subscribe/", weblate.accounts.views.subscribe, name="subscribe"),
path("watch/<name:project>/", weblate.accounts.views.watch, name="watch"),
path(
"watch/<name:project>/<name:component>/",
weblate.accounts.views.watch,
name="watch",
),
path("unwatch/<name:project>/", weblate.accounts.views.unwatch, name="unwatch"),
path(
"mute/<name:project>/<name:component>/",
weblate.accounts.views.mute_component,
name="mute",
),
path("mute/<name:project>/", weblate.accounts.views.mute_project, name="mute"),
path("watch/<object_path:path>/", weblate.accounts.views.watch, name="watch"),
path("unwatch/<object_path:path>/", weblate.accounts.views.unwatch, name="unwatch"),
path("mute/<object_path:path>/", weblate.accounts.views.mute, name="mute"),
path("remove/", weblate.accounts.views.user_remove, name="remove"),
path("confirm/", weblate.accounts.views.confirm, name="confirm"),
path("login/", weblate.accounts.views.WeblateLoginView.as_view(), name="login"),
Expand Down
45 changes: 21 additions & 24 deletions weblate/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
from weblate.auth.models import Invitation, User, get_auth_keys
from weblate.auth.utils import format_address
from weblate.logger import LOGGER
from weblate.trans.models import Change, Component, Suggestion, Translation
from weblate.trans.models import Change, Component, Project, Suggestion, Translation
from weblate.trans.models.component import translation_prefetch_tasks
from weblate.trans.models.project import prefetch_project_flags
from weblate.utils import messages
Expand All @@ -108,7 +108,7 @@
from weblate.utils.request import get_ip_address, get_user_agent
from weblate.utils.stats import prefetch_stats
from weblate.utils.token import get_token
from weblate.utils.views import get_component, get_paginator, get_project
from weblate.utils.views import get_paginator, parse_path_type

CONTACT_TEMPLATE = """
Message from %(name)s <%(email)s>:
Expand Down Expand Up @@ -1022,35 +1022,37 @@ def userdata(request):

@require_POST
@login_required
def watch(request, project, component=None):
def watch(request, path):
user = request.user
if component:
redirect_obj = component_obj = get_component(request, project, component)
obj = component_obj.project
redirect_obj = obj = parse_path_type(request, path, (Component, Project))
if isinstance(obj, Component):
project = obj.project

# Mute project level subscriptions
mute_real(user, scope=SCOPE_PROJECT, component=None, project=obj)
mute_real(user, scope=SCOPE_PROJECT, component=None, project=project)
# Manually enable component level subscriptions
for default_subscription in user.subscription_set.filter(scope=SCOPE_WATCHED):
subscription, created = user.subscription_set.get_or_create(
notification=default_subscription.notification,
scope=SCOPE_COMPONENT,
component=component_obj,
component=obj,
project=None,
defaults={"frequency": default_subscription.frequency},
)
if not created and subscription.frequency != default_subscription.frequency:
subscription.frequency = default_subscription.frequency
subscription.save(update_fields=["frequency"])
else:
redirect_obj = obj = get_project(request, project)

# Watch project
obj = project
user.profile.watched.add(obj)
return redirect(redirect_obj)


@require_POST
@login_required
def unwatch(request, project):
obj = get_project(request, project)
def unwatch(request, path):
obj = parse_path_type(request, path, (Project,))
request.user.profile.watched.remove(obj)
request.user.subscription_set.filter(
Q(project=obj) | Q(component__project=obj)
Expand All @@ -1074,18 +1076,13 @@ def mute_real(user, **kwargs):

@require_POST
@login_required
def mute_component(request, project, component):
obj = get_component(request, project, component)
mute_real(request.user, scope=SCOPE_COMPONENT, component=obj, project=None)
return redirect(
"{}?notify_component={}#notifications".format(reverse("profile"), obj.pk)
)


@require_POST
@login_required
def mute_project(request, project):
obj = get_project(request, project)
def mute(request, path):
obj = parse_path_type(request, path, (Component, Project))
if isinstance(obj, Component):
mute_real(request.user, scope=SCOPE_COMPONENT, component=obj, project=None)
return redirect(
"{}?notify_component={}#notifications".format(reverse("profile"), obj.pk)
)
mute_real(request.user, scope=SCOPE_PROJECT, component=None, project=obj)
return redirect(
"{}?notify_project={}#notifications".format(reverse("profile"), obj.pk)
Expand Down
9 changes: 1 addition & 8 deletions weblate/addons/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,7 @@ def save(
)

def get_absolute_url(self):
return reverse(
"addon-detail",
kwargs={
"project": self.component.project.slug,
"component": self.component.slug,
"pk": self.pk,
},
)
return reverse("addon-detail", kwargs={"pk": self.pk})

def store_change(self, action):
Change.objects.create(
Expand Down
50 changes: 30 additions & 20 deletions weblate/addons/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,30 @@
from django.views.generic import ListView, UpdateView

from weblate.addons.models import ADDONS, Addon
from weblate.trans.models import Component
from weblate.utils import messages
from weblate.utils.views import ComponentViewMixin
from weblate.utils.views import PathViewMixin


class AddonViewMixin(ComponentViewMixin):
class AddonList(PathViewMixin, ListView):
paginate_by = None
model = Addon
supported_path_types = (Component,)

def get_queryset(self):
component = self.get_component()
if not self.request.user.has_perm("component.edit", component):
if not self.request.user.has_perm("component.edit", self.component):
raise PermissionDenied("Can not edit component")
self.kwargs["component_obj"] = component
return Addon.objects.filter_component(component)
self.kwargs["component_obj"] = self.component
return Addon.objects.filter_component(self.component)

def get_success_url(self):
component = self.get_component()
return reverse(
"addons",
kwargs={"project": component.project.slug, "component": component.slug},
)
return reverse("addons", kwargs=self.kw_component)

def redirect_list(self, message=None):
if message:
messages.error(self.request, message)
return redirect(self.get_success_url())


class AddonList(AddonViewMixin, ListView):
paginate_by = None
model = Addon

def get_context_data(self, **kwargs):
result = super().get_context_data(**kwargs)
component = self.kwargs["component_obj"]
Expand All @@ -54,8 +49,12 @@ def get_context_data(self, **kwargs):
)
return result

def get(self, request, **kwargs):
self.component = self.get_path_object()
return super().get(request, **kwargs)

def post(self, request, **kwargs):
component = self.get_component()
self.component = component = self.get_path_object()
component.acting_user = request.user
name = request.POST.get("name")
addon = ADDONS.get(name)
Expand Down Expand Up @@ -99,7 +98,7 @@ def post(self, request, **kwargs):
)


class AddonDetail(AddonViewMixin, UpdateView):
class AddonDetail(UpdateView):
model = Addon
template_name_suffix = "_detail"

Expand All @@ -115,10 +114,21 @@ def get_context_data(self, **kwargs):
result["addon"] = self.object.addon
return result

def get_success_url(self):
return reverse("addons", kwargs={"path": self.object.component.get_url_path()})

def get_object(self):
obj = super().get_object()
if not self.request.user.has_perm("component.edit", obj.component):
raise PermissionDenied("Can not edit component")
return obj

def post(self, request, *args, **kwargs):
obj = self.get_object()
obj.component.acting_user = request.user
if "delete" in request.POST:
obj.component.acting_user = request.user
obj.delete()
return self.redirect_list()
return redirect(
reverse("addons", kwargs={"path": obj.component.get_url_path()})
)
return super().post(request, *args, **kwargs)
41 changes: 16 additions & 25 deletions weblate/checks/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ def test_check(self):
self.assertRedirects(
response,
reverse(
"show_check_project",
kwargs={"name": "same", "project": self.project.slug},
"show_check_path",
kwargs={"name": "same", "path": self.project.get_url_path()},
),
)
response = self.client.get(
Expand All @@ -60,70 +60,61 @@ def test_check(self):
def test_project(self):
response = self.client.get(
reverse(
"show_check_project",
kwargs={"name": "same", "project": self.project.slug},
"show_check_path",
kwargs={"name": "same", "path": self.project.get_url_path()},
)
)
self.assertContains(response, "/same/")

response = self.client.get(
reverse(
"show_check_project",
kwargs={"name": "same", "project": self.project.slug},
"show_check_path",
kwargs={"name": "same", "path": self.project.get_url_path()},
),
{"lang": "cs"},
)
self.assertContains(response, "/same/")

response = self.client.get(
reverse(
"show_check_project",
kwargs={"name": "ellipsis", "project": self.project.slug},
"show_check_path",
kwargs={"name": "ellipsis", "path": self.project.get_url_path()},
)
)
self.assertContains(response, "…")

response = self.client.get(
reverse(
"show_check_project",
kwargs={"name": "non-existing", "project": self.project.slug},
"show_check_path",
kwargs={"name": "non-existing", "path": self.project.get_url_path()},
)
)
self.assertEqual(response.status_code, 404)

def test_component(self):
response = self.client.get(
reverse(
"show_check_component",
kwargs={
"name": "same",
"project": self.project.slug,
"component": self.component.slug,
},
"show_check_path",
kwargs={"name": "same", "path": self.component.get_url_path()},
)
)
self.assertContains(response, "/same/")

response = self.client.get(
reverse(
"show_check_component",
"show_check_path",
kwargs={
"name": "multiple_failures",
"project": self.project.slug,
"component": self.component.slug,
"path": self.component.get_url_path(),
},
)
)
self.assertContains(response, "/multiple_failures/")

response = self.client.get(
reverse(
"show_check_component",
kwargs={
"name": "non-existing",
"project": self.project.slug,
"component": self.component.slug,
},
"show_check_path",
kwargs={"name": "non-existing", "path": self.component.get_url_path()},
)
)
self.assertEqual(response.status_code, 404)
Loading

0 comments on commit 633f704

Please sign in to comment.