-
Notifications
You must be signed in to change notification settings - Fork 21
feat: CourseKeyField can now be optionally case-sensitive, has default max_length [FC-0117] #426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b180195
ad2a7f3
de35180
293058a
1721be1
b7937f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,7 @@ | |
|
|
||
| try: | ||
| from django.core.exceptions import ValidationError | ||
| from django.db.models import CharField | ||
| from django.db.models import CharField, Field | ||
| from django.db.models.lookups import IsNull | ||
| except ImportError: # pragma: no cover | ||
| # Django is unavailable, none of the classes below will work, | ||
|
|
@@ -96,8 +96,9 @@ class OpaqueKeyField(CreatorMixin, CharField): | |
|
|
||
| def __init__(self, *args, **kwargs): | ||
| if self.KEY_CLASS is None: | ||
| raise ValueError('Must specify KEY_CLASS in OpaqueKeyField subclasses') | ||
|
|
||
| raise ValueError('Must specify KEY_CLASS in OpaqueKeyField subclasses') # pragma: no cover | ||
| kwargs.setdefault("max_length", 255) # Default max length for all opaque key fields | ||
| self.case_sensitive = kwargs.pop("case_sensitive", False) # see self.db_parameters() for details | ||
| super().__init__(*args, **kwargs) | ||
|
|
||
| def to_python(self, value): # pylint: disable=missing-function-docstring | ||
|
|
@@ -164,6 +165,44 @@ def run_validators(self, value): | |
|
|
||
| return super().run_validators(value) | ||
|
|
||
| def db_parameters(self, connection): | ||
| """ | ||
| Return database parameters for this field. This adds collation info, to | ||
| make the key field case-sensitive (optionally). | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I keep seeing PRs about adding/fixing postgres support to the platform, so I wonder if it's worth making the case-insensitivity work on postgres too. At the very least, could you add your postgres note from the PR description here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From my research, getting case-insensitivity to work on PostgreSQL is not possible without a migration to create a case-insensitive collation (once, for the whole database), which we could then use. Since this library has no migrations, I don't think we should do that. I can definitely add the note here though. |
||
| Previously, these fields were case-sensitive on SQLite and | ||
| case-insensitive on MySQL, which was not ideal. Now they are generally | ||
| case-insensitive by default (for backwards compatibility), and | ||
| case-sensitive if case_sensitive=True is specified (recommended!). | ||
| """ | ||
| db_params = Field.db_parameters(self, connection) | ||
|
|
||
| if connection.vendor == "sqlite": | ||
| db_params["collation"] = "BINARY" if self.case_sensitive else "NOCASE" | ||
| elif connection.vendor == "mysql": # pragma: no cover | ||
| db_params["collation"] = "utf8mb4_bin" if self.case_sensitive else "utf8mb4_unicode_ci" | ||
| # We're using utf8mb4_unicode_ci to keep MariaDB compatibility, since their collation support diverges after | ||
| # this. MySQL is now on utf8mb4_0900_ai_ci based on Unicode 9, while MariaDB has uca1400_ai_ci (Unicode 14). | ||
| elif connection.vendor == "postgresql": # pragma: no cover | ||
| # PostgreSQL only provides case-sensitive collations by default. To create a case-insensitive column, | ||
| # we'd first need to use a migration to create a custom case-insensitive collation, but we don't use | ||
| # migrations for this opaque-keys library. Anyhow, using case-sensitivity across the board is less | ||
| # likely to cause bugs than the opposite. | ||
| pass | ||
|
|
||
| return db_params | ||
|
|
||
| def deconstruct(self): | ||
| """ | ||
| How to serialize our Field for the migration file. | ||
|
|
||
| Just add our custom "case_sensitive" field if needed. | ||
| """ | ||
| name, path, args, kwargs = super().deconstruct() | ||
| if self.case_sensitive: | ||
| kwargs["case_sensitive"] = True | ||
| return name, path, args, kwargs | ||
|
|
||
|
|
||
| class OpaqueKeyFieldEmptyLookupIsNull(IsNull): | ||
| """ | ||
|
|
@@ -191,6 +230,10 @@ class LearningContextKeyField(OpaqueKeyField): | |
| CourseKeyField instead, but if you are writing something more generic that | ||
| could apply to any learning context (libraries, etc.), use this instead of | ||
| CourseKeyField. | ||
|
|
||
| It is highly recommended to specify `case_sensitive=True`, because we | ||
| generally want the performance, exactness, and consistency of case-sensitive | ||
| values, but the default behavior is case-insensitive (except on PostgreSQL). | ||
| """ | ||
| description = "A LearningContextKey object, saved to the DB in the form of a string" | ||
| KEY_CLASS = LearningContextKey | ||
|
|
@@ -205,6 +248,10 @@ class LearningContextKeyField(OpaqueKeyField): | |
| class CourseKeyField(OpaqueKeyField): | ||
| """ | ||
| A django Field that stores a CourseKey object as a string. | ||
|
|
||
| It is highly recommended to specify `case_sensitive=True`, because we | ||
| generally want the performance, exactness, and consistency of case-sensitive | ||
| values, but the default behavior is case-insensitive (except on PostgreSQL). | ||
| """ | ||
| description = "A CourseKey object, saved to the DB in the form of a string" | ||
| KEY_CLASS = CourseKey | ||
|
|
@@ -216,6 +263,10 @@ class CourseKeyField(OpaqueKeyField): | |
| class UsageKeyField(OpaqueKeyField): | ||
| """ | ||
| A django Field that stores a UsageKey object as a string. | ||
|
|
||
| It is highly recommended to specify `case_sensitive=True`, because we | ||
| generally want the performance, exactness, and consistency of case-sensitive | ||
| values, but the default behavior is case-insensitive (except on PostgreSQL). | ||
| """ | ||
| description = "A Location object, saved to the DB in the form of a string" | ||
| KEY_CLASS = UsageKey | ||
|
|
@@ -227,6 +278,10 @@ class UsageKeyField(OpaqueKeyField): | |
| class ContainerKeyField(OpaqueKeyField): | ||
| """ | ||
| A django Field that stores a ContainerKey object as a string. | ||
|
|
||
| It is highly recommended to specify `case_sensitive=True`, because we | ||
| generally want the performance, exactness, and consistency of case-sensitive | ||
| values, but the default behavior is case-insensitive (except on PostgreSQL). | ||
| """ | ||
| description = "A Location object, saved to the DB in the form of a string" | ||
| KEY_CLASS = ContainerKey | ||
|
|
@@ -238,6 +293,10 @@ class ContainerKeyField(OpaqueKeyField): | |
| class CollectionKeyField(OpaqueKeyField): | ||
| """ | ||
| A django Field that stores a CollectionKey object as a string. | ||
|
|
||
| It is highly recommended to specify `case_sensitive=True`, because we | ||
| generally want the performance, exactness, and consistency of case-sensitive | ||
| values, but the default behavior is case-insensitive (except on PostgreSQL). | ||
| """ | ||
| description = "A Location object, saved to the DB in the form of a string" | ||
| KEY_CLASS = CollectionKey | ||
|
|
@@ -249,6 +308,10 @@ class CollectionKeyField(OpaqueKeyField): | |
| class LocationKeyField(UsageKeyField): | ||
| """ | ||
| A django Field that stores a UsageKey object as a string. | ||
|
|
||
| It is highly recommended to specify `case_sensitive=True`, because we | ||
| generally want the performance, exactness, and consistency of case-sensitive | ||
| values, but the default behavior is case-insensitive (except on PostgreSQL). | ||
| """ | ||
|
|
||
| def __init__(self, *args, **kwargs): | ||
|
|
@@ -259,6 +322,10 @@ def __init__(self, *args, **kwargs): | |
| class BlockTypeKeyField(OpaqueKeyField): | ||
| """ | ||
| A django Field that stores a BlockTypeKey object as a string. | ||
|
|
||
| It is highly recommended to specify `case_sensitive=True`, because we | ||
| generally want the performance, exactness, and consistency of case-sensitive | ||
| values, but the default behavior is case-insensitive (except on PostgreSQL). | ||
| """ | ||
| description = "A BlockTypeKey object, saved to the DB in the form of a string." | ||
| KEY_CLASS = BlockTypeKey | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.