Skip to content

Commit

Permalink
Merge pull request #4 from Nabute/feature/translation-pk-flexibility-…
Browse files Browse the repository at this point in the history
…and-serializer

Feature: Add Translation Table Primary Key Flexibility and Nested Translation Serializer
  • Loading branch information
Nabute authored Nov 11, 2024
2 parents 3ab08f5 + 42fe323 commit 9e645d1
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 53 deletions.
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,70 @@
## [v0.1.5] - 2024-11-11

This release introduces two major enhancements to `Lisan`'s translation model functionality, improving flexibility in primary key configurations and structuring translation data for API responses.

### Added
- **Flexible Primary Key Configuration for Translation Tables**:
- Added support to configure primary key type for translation (Lisan) tables, allowing the use of either `BigInt` or `UUID` as primary keys.
- Configurable globally via `settings.py` (`LISAN_PRIMARY_KEY_TYPE`) or per model by setting the `lisan_primary_key_type` attribute.
- Ensures flexibility for projects that require UUIDs or BigInts based on specific requirements or database configurations.

- **Nested Translation Serializer for Structured API Responses**:
- Introduced a `TranslationSerializer` to handle multilingual data in API responses, providing a structured, nested format.
- Integrated `TranslationSerializer` within `LisanSerializerMixin`, enabling organized representation of translations in API responses.
- Allows each translation entry to include fields such as `language_code` and all specified translatable fields, making it easier to work with multilingual data in client applications.

### Improved
- **Dynamic Primary Key Assignment for Lisan Models**:
- Enhanced the `LisanModelMeta` metaclass to detect and apply the specified primary key type dynamically, either at the model level or globally.
- Ensured that the primary key type for Many-to-Many join tables remains `AutoField` even when the `Lisan` model uses `UUIDField` as its primary key, simplifying compatibility with Django’s default join tables.

### Configuration Changes
- **New Settings**:
- `LISAN_PRIMARY_KEY_TYPE`: Allows configuration of the primary key type for translation tables globally. Options include `BigAutoField` (default) and `UUIDField`.

### Migration Notes
- A new migration is required if you change the primary key type for existing translation tables. After updating, use the following commands:

```bash
python manage.py makemigrations
python manage.py migrate
```

### Example Usage

- **Primary Key Configuration**: Define primary key type globally in `settings.py` or per model:
```python
# In settings.py
LISAN_PRIMARY_KEY_TYPE = models.UUIDField

# Per model configuration
class MyModel(LisanModelMixin, models.Model):
lisan_primary_key_type = models.UUIDField
```

- **Translation Serializer**: Access structured translation data in API responses with `TranslationSerializer`:
```json
{
"id": 1,
"title": "Sample Title",
"description": "Sample Description",
"translations": [
{
"language_code": "am",
"title": "ምሳሌ ርእስ",
"description": "ምሳሌ መግለጫ"
},
{
"language_code": "en",
"title": "Sample Title",
"description": "Sample Description"
}
]
}
```

---

## [v0.1.4] - 2024-10-07

This release introduces improvements in the translation validation process for partial updates, ensuring that translations are properly validated unless explicitly omitted in partial updates.
Expand Down
49 changes: 40 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
## Features

- **Automatic Translation Models:** Automatically generate translation models for your fields.
- **Flexible Primary Key Configuration:** Use either BigInt or UUID as primary keys for translation tables, configurable globally or per model.
- **Admin Integration:** Seamlessly manage translations through the Django admin interface.
- **Fallback Mechanism:** Fallback to the default language if a translation is not available.
- **Dynamic Getter Methods:** Automatically generate methods to access translated fields.
Expand Down Expand Up @@ -44,7 +45,7 @@ pip install lisan

### 1. Configuring Lisan

To start using `Lisan` in your project, you need to configure the language settings and middleware in your Django settings file.
To start using `Lisan` in your project, configure the language settings, middleware, and primary key type (if needed) in your Django settings file.

#### Step 1.0: Add Lisan Language Settings

Expand All @@ -55,7 +56,20 @@ LISAN_FALLBACK_LANGUAGES = ['fr', 'es', 'en'] # Customize fallback languages
LISAN_DEFAULT_TRANSLATION_SERVICE = 'yourapp.google_translate_service.GoogleTranslateService' # Pluggable translation service
```

#### Step 1.1: Add Lisan Middleware
#### Step 1.1: Configure Primary Key Type (Optional)

You can configure `Lisan` to use either `BigInt` or `UUID` as the primary key for translation tables.

To set this globally, use the `LISAN_PRIMARY_KEY_TYPE` setting in `settings.py`:

```python
from django.db import models
LISAN_PRIMARY_KEY_TYPE = models.UUIDField # Options: models.BigAutoField (default) or models.UUIDField
```

Alternatively, define `lisan_primary_key_type` on specific models to override the global setting.

#### Step 1.2: Add Lisan Middleware

Make sure to include `Lisan`'s middleware in your `MIDDLEWARE` settings for automatic language detection and management:

Expand Down Expand Up @@ -86,6 +100,9 @@ class Snippet(LisanModelMixin, models.Model):
description = models.TextField(blank=True, default='')
created = models.DateTimeField(auto_now_add=True)

# Optionally specify UUIDField as primary key for translation tables
lisan_primary_key_type = models.UUIDField

class Meta:
ordering = ['created']
```
Expand Down Expand Up @@ -162,9 +179,9 @@ To create a snippet with translations, send a `POST` request to the appropriate
}
```

### 2. Retrieving a Snippet with a Specific Translation
### 2. Retrieving a Snippet with Translations Using Nested Translation Serializer

To retrieve a snippet in a specific language, send a `GET` request with the appropriate `Accept-Language` header to specify the desired language (e.g., `am` for Amharic).
To retrieve translations for a snippet, use the `TranslationSerializer` to structure the translations in a nested format.

**Request Example**:

Expand All @@ -173,15 +190,27 @@ GET /api/snippets/1/
Accept-Language: am
```

The response will return the snippet information in the requested language if available, or it will fallback to the default language:
The response will include all translations for the snippet in a structured format:

**Response Example**:

```json
{
"id": 1,
"title": "ኮድ ቅርጸት ምሳሌ",
"description": "እንቁ ምሳሌ"
"title": "Code Snippet Example",
"description": "Example Description",
"translations": [
{
"language_code": "am",
"title": "ኮድ ቅርጸት ምሳሌ",
"description": "እንቁ ምሳሌ"
},
{
"language_code": "en",
"title": "Code Snippet Example",
"description": "Example Description"
}
]
}
```

Expand Down Expand Up @@ -219,7 +248,9 @@ from googletrans import Translator
from lisan.translation_services import BaseTranslationService

class GoogleTranslateService(BaseTranslationService):
def __init__(self):
def __init

__(self):
self.translator = Translator()

def translate(self, text, target_language):
Expand Down Expand Up @@ -263,4 +294,4 @@ If you find any issues or have suggestions for improvements, feel free to open a

## License

Lisan is licensed under the MIT License. See the [LICENSE](LICENSE) file for more information.
Lisan is licensed under the MIT License. See the [LICENSE](LICENSE) file for more information.
38 changes: 33 additions & 5 deletions lisan/metaclasses.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from django.db import models
from django.conf import settings
from django.utils.translation import gettext_lazy as _


def create_lisan_model(model_cls, fields):
def create_lisan_model(
model_cls, fields, primary_key_type=models.BigAutoField):
"""
Dynamically create a Lisan model for the given model class.
Expand Down Expand Up @@ -37,6 +39,19 @@ class Meta:
),
}

if primary_key_type == models.UUIDField:
import uuid
# Configure UUIDField with auto-generation
attrs['id'] = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False,
verbose_name=_("id")
)
else:
# Default primary key field setup
attrs['id'] = primary_key_type(primary_key=True)

# Add the specified fields to the Lisan model
for field_name, field in fields.items():
attrs[field_name] = field
Expand Down Expand Up @@ -86,24 +101,37 @@ def __new__(cls, name, bases, attrs):
"""
if 'LisanModelMixin' in [base.__name__ for base in bases]:
lisan_fields = attrs.get('lisan_fields')

# If `lisan_fields` is not defined, raise an exception
if lisan_fields is None:
raise AttributeError(
f"{name} must define 'lisan_fields' when using LisanModelMixin."
f"{name} must define 'lisan_fields' when using LisanModelMixin." # noqa
)

# Filter translatable fields by checking if they are defined in lisan_fields
# Filter translatable fields by checking if they are
# defined in lisan_fields
translatable_fields = {
key: value for key, value in attrs.items()
if isinstance(value, models.Field) and key in lisan_fields
}

# Determine primary key type, checking model-specific
# setting first, then global
primary_key_type = attrs.get(
'lisan_primary_key_type', # Model-specific setting
getattr(
settings,
'LISAN_PRIMARY_KEY_TYPE',
models.BigAutoField
)
)

# Create the new model class
new_class = super().__new__(cls, name, bases, attrs)

# Generate the Lisan model
lisan_model = create_lisan_model(new_class, translatable_fields)
lisan_model = create_lisan_model(
new_class, translatable_fields, primary_key_type)
setattr(new_class, 'Lisan', lisan_model)

# Add a ForeignKey linking the original model to the Lisan model
Expand Down
16 changes: 16 additions & 0 deletions lisan/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,29 @@ def set_lisan(self, language_code, **lisan_fields):
f"Field '{field_name}' does not exist in the "
"translation model."
)

primary_key_field = getattr(self.Lisan._meta, 'pk', None)
if primary_key_field and primary_key_field.name != 'id':
primary_key_value = lisan_fields.pop(
primary_key_field.name, None)
else:
primary_key_value = None

# Explicitly set the foreign key to self (the instance)
# TODO: this is not capturing the UUID for the id
lisan = self.Lisan(
**lisan_fields,
language_code=language_code,
**{self._meta.model_name: self}
)

if primary_key_value is not None:
setattr(
lisan,
primary_key_field.name,
primary_key_value
)

lisan.save()
self.lisans.add(lisan)
else:
Expand Down
Loading

0 comments on commit 9e645d1

Please sign in to comment.