Skip to content

Commit

Permalink
projects: add docs and test for m2m topics
Browse files Browse the repository at this point in the history
  • Loading branch information
goapunk authored and m4ra committed Dec 12, 2023
1 parent e26c42f commit f7e08b6
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 0 deletions.
10 changes: 10 additions & 0 deletions adhocracy4/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,19 @@


class Topic(models.Model):
"""
A class that provides topics to Project class through an m2m field.
Topic objects are created from a TopicEnum class,
thus the TopicEnum.name is saved as the Topic.code field.
"""

code = models.CharField(blank=True, max_length=10, unique=True)

def __str__(self):
"""
Returns the descriptive and translatable TopicEnum label.
"""

if hasattr(settings, "A4_PROJECT_TOPICS"):
topics_enum = import_string(settings.A4_PROJECT_TOPICS)
return str(topics_enum(self.code).label)
Expand Down
31 changes: 31 additions & 0 deletions adhocracy4/test/factories/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from adhocracy4.phases.models import Phase
from adhocracy4.projects.enums import Access
from adhocracy4.projects.models import Project
from adhocracy4.projects.models import Topic
from tests.project.enums import TopicEnum


class GroupFactory(factory.django.DjangoModelFactory):
Expand Down Expand Up @@ -86,6 +88,35 @@ def moderators(self, create, extracted, **kwargs):
for user in extracted:
self.moderators.add(user)

@factory.post_generation
def topics(self, create, extracted, **kwargs):
if not create:
return
if extracted:
for topic in extracted:
self.topics.add(topic)


class TopicFactory(factory.django.DjangoModelFactory):
"""Create Topics from the TopicEnum class
Note: This factory can only create len(TopicEnum) topics because of the unique
constraint of the Topic model
"""

class Meta:
model = Topic

code = factory.Sequence(lambda n: TopicEnum.names[n])

@factory.post_generation
def projects(self, create, extracted, **kwargs):
if not create:
return
if extracted:
for project in extracted:
self.project_set.add(project)


class ModuleFactory(factory.django.DjangoModelFactory):
class Meta:
Expand Down
5 changes: 5 additions & 0 deletions changelog/7885.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Removed

- requirements: django-multiselectfield.
Project.topics are now m2m with Topic, as the django-multiselectfield lib has not been supported.
see the [docs](https://github.com/liqd/adhocracy4/blob/main/docs/topic_enums.md) for more info.
57 changes: 57 additions & 0 deletions docs/topic_enums.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# A4 Project Topics

## Introduction
Each project has the option to choose up to two topics relevant to the project's context. For the choises of topics we used to work with a list of tuples defined in the settings directly, and the django-multiselectfield library, but we needed to replace this library as it is no longer maintained. So a `Project` object now has a many-to-many relation with `topics`, and `Topic` objects are created based on the `TopicEnum` class. Enums are also more flexible to work with, as we can call the enum values, label and code properties depending on where and how these are called in the code.

## Configuration
The topics should be defined in the settings as a string path to an Enum class (TopicEnum):
```
A4_PROJECT_TOPICS = "tests.project.enums.TopicEnum"
```
[ref: tests/project/settings](https://github.com/liqd/adhocracy4/blob/main/tests/project/settings.py)

## Development

`Project` property `topic_names` has now changed to accommodate the new TopicEnum class, mostly for html template use.
[ref: Topic/Project/topic_names](https://github.com/liqd/adhocracy4/blob/main/adhocracy4/projects/models.py)

## Migration
If a django application which is based on `adhocracy4` has projects with existing topics and using the django-multiselectfield, those will be added to a many-to-many topics field after updating `adhocracy4` and run the migrations. Your django application would need to define a TopicEnum and define its path as a string in the settings like the example in the configuration section above. The previous tuples for topic choices that used to be, for example, in the form of:
```
A4_PROJECT_TOPICS = (
("ANT", _("Anti-discrimination")),
("WOR", _("Work & economy")),
("BUI", _("Building & living")),
)
```
will now need to move to an Enum class in your django application, or also possible to move to django's `models.TextChoices`, which is a subclass of Enum and allows for applying dynamic translations for the labels with gettext `_` as in the following example:
```
class TopicEnum(models.TextChoices):
"""Choices for project topics."""
ANT = "ANT", _("Anti-discrimination"),
WOR = "WOR", _("Work & economy"),
BUI = "BUI", _("Building & living")
```

Once this is done, your django application needs to make these enums as `Topic` objects with a custom migration. Generate an empty migration with `python manage.py makemigrations <app_name> --empty`. Edit the new migration. Here some boilerplate code for such a migration:
```
from my_application.apps.projects.enums import TopicEnum
def add_topics(apps, schema_editor):
if hasattr(settings, "A4_PROJECT_TOPICS"):
Topic = apps.get_model("a4projects", "Topic")
for topic in TopicEnum:
Topic.objects.get_or_create(code=topic)
class Migration(migrations.Migration):
dependencies = [
("app_label", "xxxx_previous_migration"),
]
operations = [
migrations.RunPython(add_topics, migrations.RunPython.noop),
]
```
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def image_factory():
register(factories.UserFactory, "another_user")
register(factories.UserFactory, "staff_user", is_staff=True)
register(factories.ProjectFactory)
register(factories.TopicFactory)
register(factories.ModuleFactory)
register(factories.PhaseFactory)
register(q_factories.QuestionFactory)
Expand Down
14 changes: 14 additions & 0 deletions tests/projects/test_project_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,17 @@ def test_is_archivable(project_factory, phase_factory):
assert not project1.is_archivable
assert not project2.is_archivable
assert project3.is_archivable


@pytest.mark.django_db
def test_project_topics(project_factory, topic_factory):
project = project_factory()
assert project.topic_names == []
# delete to clear the cache of the cached_property
del project.topic_names
topic1 = topic_factory(projects=[project])
assert project.topic_names == [str(topic1)]
# delete to clear the cache of the cached_property
del project.topic_names
topic2 = topic_factory(projects=[project])
assert project.topic_names == [str(topic1), str(topic2)]

0 comments on commit f7e08b6

Please sign in to comment.