diff --git a/nonpacks/library/tests.py b/nonpacks/library/tests.py index 96b5744..ebee0ed 100644 --- a/nonpacks/library/tests.py +++ b/nonpacks/library/tests.py @@ -704,6 +704,70 @@ class UGCPermissionTests(UGCMediaTestCase, UGCGatedTestCase): # Manual species tag applied. self.assertTrue(TagList.objects.filter(project=self.project, tag__name='dragon').exists()) + def _add_manual_creator_tag(self, name='lizo'): + creator_cat = TagCategory.objects.get(slug='creator') + tag, _ = Tag.objects.get_or_create(name=name, category=creator_cat, created_by=self.alice) + TagList.objects.create(project=self.project, tag=tag, added_by=self.alice) + return tag + + def test_edit_shows_manual_creator_tag(self): + self.project.sync_creator_tags() + self._add_manual_creator_tag('lizo') + self.gate() + self.client.login(username='Alice', password='pw') + resp = self.client.get(reverse('library:project_edit', args=['test-pack'])) + # The manual creator tag (no matching account) is serialized into the editor. + self.assertContains(resp, 'creator:lizo') + + def test_edit_hides_account_backed_creator_tags(self): + ProjectContributor.objects.create(project=self.project, user=self.bob, added_by=self.alice) + self.project.sync_creator_tags() + self._add_manual_creator_tag('lizo') + self.gate() + self.client.login(username='Alice', password='pw') + resp = self.client.get(reverse('library:project_edit', args=['test-pack'])) + # Manual tag visible; account-backed creator tags (alice, bob) hidden. + self.assertContains(resp, 'creator:lizo') + self.assertNotContains(resp, 'creator:alice') + self.assertNotContains(resp, 'creator:bob') + + def test_edit_removing_manual_creator_tag_deletes_it(self): + self.project.sync_creator_tags() + self._add_manual_creator_tag('lizo') + self.gate() + self.client.login(username='Alice', password='pw') + resp = self.client.post( + reverse('library:project_edit', args=['test-pack']), + {'title': 'Test Pack', 'category': 'mod', 'description': 'x', 'tags': ''}, + ) + self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack'])) + # Manual creator tag removed from the form is now deleted... + self.assertFalse( + TagList.objects.filter(project=self.project, tag__name='lizo').exists() + ) + # ...while the account-backed owner creator tag survives. + self.assertTrue( + TagList.objects.filter(project=self.project, tag__name='alice').exists() + ) + + def test_edit_keeps_manual_creator_tag_when_resubmitted(self): + self.project.sync_creator_tags() + self._add_manual_creator_tag('lizo') + self.gate() + self.client.login(username='Alice', password='pw') + resp = self.client.post( + reverse('library:project_edit', args=['test-pack']), + {'title': 'Test Pack', 'category': 'mod', 'description': 'x', + 'tags': 'creator:lizo species:dragon'}, + ) + self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack'])) + self.assertTrue( + TagList.objects.filter(project=self.project, tag__name='lizo').exists() + ) + self.assertTrue( + TagList.objects.filter(project=self.project, tag__name='dragon').exists() + ) + class UGCBrowseTests(UGCGatedTestCase): def setUp(self): diff --git a/nonpacks/library/views.py b/nonpacks/library/views.py index dd31102..d96b5f8 100644 --- a/nonpacks/library/views.py +++ b/nonpacks/library/views.py @@ -156,13 +156,30 @@ def _tags_from_text(text): return pairs +def _managed_creator_names(project): + """Slugified usernames of the owner + contributors — the creator tags that + sync_creator_tags manages automatically. Creator tags that are NOT backed + by an account (e.g. an offline artist) are treated as manual and stay + editable/removable.""" + names = {slugify_tag(project.owner.username)} + names.update( + slugify_tag(c.user.username) + for c in project.contributors.select_related('user') + ) + return names + + def _apply_tags(project, raw_tags, actor): - """Replace a project's manual tags (auto-creating new ones). Creator tags - are managed separately by sync_creator_tags.""" + """Replace a project's manual tags (auto-creating new ones). Account-backed + creator tags (owner + contributors) are managed by sync_creator_tags and + preserved; any other creator tag is editable like any other manual tag.""" creator_cat = TagCategory.objects.filter(slug='creator').first() to_delete = project.tag_links.all() if creator_cat is not None: - to_delete = to_delete.exclude(tag__category=creator_cat) + to_delete = to_delete.exclude( + tag__category=creator_cat, + tag__name__in=_managed_creator_names(project), + ) to_delete.delete() for category_slug, name in _tags_from_text(raw_tags): @@ -178,11 +195,18 @@ def _apply_tags(project, raw_tags, actor): def _current_tags(project): - """Serialize a project's non-creator tags back to 'category:name' form.""" + """Serialize a project's editable tags back to 'category:name' form. + + Account-backed creator tags (owner + contributors) are auto-managed and + hidden; manual creator tags (no matching account) remain visible so they + can be removed.""" creator_cat = TagCategory.objects.filter(slug='creator').first() qs = project.tag_links.select_related('tag__category').all() if creator_cat is not None: - qs = qs.exclude(tag__category=creator_cat) + qs = qs.exclude( + tag__category=creator_cat, + tag__name__in=_managed_creator_names(project), + ) return ' '.join(f'{tl.tag.category.slug}:{tl.tag.name}' for tl in qs) diff --git a/nonpacks/templates/base.html b/nonpacks/templates/base.html index c97c416..cc972c4 100644 --- a/nonpacks/templates/base.html +++ b/nonpacks/templates/base.html @@ -81,6 +81,11 @@ API Tokens + {% if user.is_staff %} + + Admin panel + + {% endif %}