diff --git a/fxsharing/shares/migrations/0003_alter_link_options_alter_share_options_link_position_and_more.py b/fxsharing/shares/migrations/0003_alter_link_options_alter_share_options_link_position_and_more.py new file mode 100644 index 0000000..61d401d --- /dev/null +++ b/fxsharing/shares/migrations/0003_alter_link_options_alter_share_options_link_position_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 6.0.6 on 2026-07-06 17:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shares', '0002_share_type'), + ] + + operations = [ + migrations.AlterModelOptions( + name='link', + options={'ordering': ('position',)}, + ), + migrations.AlterModelOptions( + name='share', + options={'ordering': ('position',)}, + ), + migrations.AddField( + model_name='link', + name='position', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='share', + name='position', + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/fxsharing/shares/models.py b/fxsharing/shares/models.py index 97495f1..00be2bb 100644 --- a/fxsharing/shares/models.py +++ b/fxsharing/shares/models.py @@ -1,3 +1,4 @@ +import heapq import math import secrets import string @@ -64,6 +65,7 @@ class Share(models.Model): blank=True, related_name="nested_shares", ) + position = models.PositiveIntegerField(default=0) expires_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) deleted_at = models.DateTimeField(null=True, blank=True) @@ -71,6 +73,9 @@ class Share(models.Model): objects = SoftDeleteManager() all_objects = models.Manager.from_queryset(SoftDeleteQuerySet)() + class Meta: + ordering = ("position",) + @property def is_expired(self): if self.status == ShareStatus.EXPIRED: @@ -117,9 +122,12 @@ def to_dict(self, this_only=False): ) if not this_only: - links = [link.to_dict() for link in self.links.all()] - nested_shares = [s.to_dict() for s in self.nested_shares.all()] - this["links"] = links + nested_shares + children = heapq.merge( + self.links.all(), + self.nested_shares.all(), + key=lambda obj: obj.position, + ) + this["links"] = [child.to_dict() for child in children] if self.parent_share: this["parent_share"] = self.parent_share.to_dict(this_only=True) @@ -135,6 +143,7 @@ def get_queryset(self): class Link(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) share = models.ForeignKey(Share, on_delete=models.CASCADE, related_name="links") + position = models.PositiveIntegerField(default=0) title = models.CharField(max_length=100, blank=True) url = models.URLField(max_length=4000) safety_status = models.CharField( @@ -150,6 +159,9 @@ class Link(models.Model): objects = LinkManager() all_objects = models.Manager() + class Meta: + ordering = ("position",) + def __str__(self): return self.title or self.url diff --git a/fxsharing/shares/tests.py b/fxsharing/shares/tests.py index c0c0c0a..119e3f6 100644 --- a/fxsharing/shares/tests.py +++ b/fxsharing/shares/tests.py @@ -138,6 +138,24 @@ def test_to_dict_includes_new_fields(self): assert d["preview_image_url"] == "https://example.com/img.png" assert d["favicon_url"] == "https://example.com/favicon.png" + def test_to_dict_orders_children_by_position(self): + Link.objects.create(share=self.share, url="https://c.example", position=2) + Link.objects.create(share=self.share, url="https://a.example", position=0) + Share.objects.create( + title="Folder", + user=self.user, + type="bookmarks", + parent_share=self.share, + position=1, + ) + + children = self.share.to_dict()["links"] + assert [c.get("url") or c["title"] for c in children] == [ + "https://a.example", + "Folder", + "https://c.example", + ] + class TestCreateShare(TestCase): @classmethod diff --git a/fxsharing/shares/views.py b/fxsharing/shares/views.py index 7416646..cf6a88b 100644 --- a/fxsharing/shares/views.py +++ b/fxsharing/shares/views.py @@ -153,12 +153,13 @@ def active_share_count(user): @transaction.atomic -def create_share_from_data(data, user, parent_share=None): +def create_share_from_data(data, user, parent_share=None, position=0): share = Share.objects.create( user=user, title=data["title"], type=data["type"], parent_share=parent_share, + position=position, expires_at=( timezone.now() + timedelta(days=SHARE_EXPIRY_DAYS) if parent_share is None @@ -167,11 +168,18 @@ def create_share_from_data(data, user, parent_share=None): ) links = [] - for obj in data["links"]: + for index, obj in enumerate(data["links"]): if obj.get("url"): - links.append(Link(share=share, title=obj.get("title", ""), url=obj["url"])) + links.append( + Link( + share=share, + title=obj.get("title", ""), + url=obj["url"], + position=index, + ) + ) elif obj.get("links"): - create_share_from_data(obj, user=user, parent_share=share) + create_share_from_data(obj, user=user, parent_share=share, position=index) Link.objects.bulk_create(links)