-
Notifications
You must be signed in to change notification settings - Fork 11
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
Another pass at basic content models #13
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I took so long to review this. Most of my comments are just me trying to make sure I grok everything.
projects/dev.py
Outdated
# Learning Core Apps | ||
'openedx_learning.apps.core.compose.apps.ComposeConfig', | ||
'openedx_learning.apps.core.itemstore.apps.ItemStoreConfig', | ||
'openedx_learning.apps.core.publish.apps.PublishConfig', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm aware these comments are superficial & tangential to the PR, but I ought to express them now before anyone starts using this repo :)
- The
apps
namespace package seems unnecessary and out-of-step with other edx-platform libraries. Why not justopenedx_learning.core.itemstore
? - I notice that you are using verbs for some of the core app names. Wouldn't it be more standard to use nouns? (
composition
,publishing
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
apps
namespace package seems unnecessary and out-of-step with other edx-platform libraries. Why not justopenedx_learning.core.itemstore
?
Mostly because I wanted a place to hang lib
for things like the field types.
I notice that you are using verbs for some of the core app names. Wouldn't it be more standard to use nouns? (
composition
,publishing
)
Yeah, I was going for the verb form for a while before because it was shorter, before I landed on itemstore
for one of them. I'll switch it back over to nouns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly because I wanted a place to hang lib for things like the field types.
The 2nd-level packages could be openedx_learning.core
, openedx_learning.lib
, and openedx_learning.contrib
.
I remember as a new dev being frazzled by the long import paths of openedx.core.djangoapps...
in edx-platform. I could never remember the paths; I was always copying and pasting, and it made the platform feel more overwhelming.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I'll give that a shot. I do like not having apps
appear twice in that path (package + apps.py). Thank you!
data_bytes = xml_file_path.read_bytes() | ||
hash_digest = create_hash_digest(data_bytes) | ||
data_str = codecs.decode(data_bytes, 'utf-8') | ||
mime_type = f'application/vnd.openedx.xblock.{block_type}+xml' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
XBlocks are installed under the entrypoint xblock.v1
. I don't think I have my head around the potential consequences of which MIME types we choose, so I'll leave it up to you whether or not you want to add the "v1" namespace here.
mime_type = f'application/vnd.openedx.xblock.{block_type}+xml' | |
mime_type = f'application/vnd.openedx.xblock.v1.{block_type}+xml' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right. I guess it doesn't hurt to add the v1, though I'm skeptical we'll ever have a v2. (I'm guessing we'd cali it something entirely different, to avoid confusion.) Will fix.
continue | ||
|
||
|
||
static_file_paths = frozenset( | ||
f"static/{static_reference_path}" | ||
for static_reference_path in re.findall(static_files_regex, data_str) | ||
) | ||
for static_file_path in static_file_paths: | ||
if static_file_path in static_asset_paths_to_atom_ids: | ||
atom_id = static_asset_paths_to_atom_ids[static_file_path] | ||
ContentObjectPart.objects.create(content_object=content_obj, content_atom_id=atom_id, identifier=static_file_path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dead code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh, right, because I didn't ever put the static asset mapping stuff back. Thank you.
return paths_to_item_raw_ids | ||
|
||
|
||
def import_block_type(self, block_type, content_path, static_asset_paths_to_atom_ids, item_raw_id_cache, now): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉 I know it's just a temporary management command, but it is cool and helpful for me to read some code that uses these models that we've been discussing!
class Meta: | ||
constraints = [ | ||
models.UniqueConstraint( | ||
fields=["learning_context_id", "identifier"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From this I'm deducing that two Items in different LearningContexts could share the same identifier. Is that right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. I think that in general, the identifier
fields have to be namespaced so that it's entirely local to one particular LearningContext.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the case of XBlocks, do you see this identifier
as being the usage key (block-v1:a+b+c+type@video+block@xyz
) or the block id (just xyz
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer not to have the full learning context key encoded, but since we currently allow blocks of different types to have the same ID, I think we have to encode at least something like video+xyz
|
||
TODO: Size limit thoughts–configurable? Start at 10 MB? | ||
""" | ||
learning_context = models.ForeignKey(LearningContext, on_delete=models.CASCADE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for some item_info
, will item_info.learning_context
always be equal to item_info.item_raw.learning_context
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. It's mostly there for ownership/cleanup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool. This would be helpful to note in a comment.
uuid = immutable_uuid_field() | ||
item_info = models.ForeignKey('ItemInfo', on_delete=models.RESTRICT) | ||
item = models.ForeignKey(Item, on_delete=models.CASCADE) | ||
title = models.CharField(max_length=1000, blank=True, null=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is title
here and not in ItemInfo? Is it because it's example of policy, which we're distinguishing from metadata (like type
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Renaming is desirable for libraries, because you don't know what kind of convention that library is using, e.g. "Problem 22a" may not make as much sense for the new context it's being used in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be title_override
then to clarify that you can optionally stick with whatever the library author gave as a title?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm inclined to not use the _override
convention because the default content in the library would also be using this field (just under the content library's learning context).
Though maybe the title should be lifted out into a separate model? Maybe even one with language awareness?
Wait... maybe ItemRaw:ItemVersion is M:1?
I'll make comments along both of those lines for future followup, but won't make any immediate model changes.
# TODO: These pointers to the latest published version are convenient, but | ||
# we don't currently have the data integrity guarantees to make sure that | ||
# multiple versions aren't active at the same time. Maybe add a ref to the | ||
# Item model in LearningContextVersionItemVersion, so we can force that | ||
# constraint? | ||
published_version = models.ForeignKey('ItemVersion', on_delete=models.RESTRICT, null=True, related_name="+") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
knowledge check: this is secondary source of information, since one could authoritatively determine this by asking the publish
app what the published LearningContextVersion is, and then looking at the ItemVersions within that LCV, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. And maybe it's not a great idea to have redundancy like this. The main motivation for this was that a lot of the time, you want to bind data against the abstract notion of a piece of content across all its versions (e.g. most student state), and I wanted a really convenient way to get to the current "live" version from there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That does sound convenient. I guess, you're either going to end up needing to keep this field up-to-date, or you're going to end up with a lot of requests making the same query ("what's the published version?"). I don't think I have the experience to know which of these is an easier challenge to manage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about making published_version
a [memoized] python getter that uses the publish app to return the published version? Or do you mean specifically you want to be able to use the django ORM methods on this published_version
field so you can select_related
and do things like filtering queries?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or do you mean specifically you want to be able to use the django ORM methods on this published_version field so you can select_related and do things like filtering queries?
Yeah, exactly this. I want to make sure we can still do efficient filtering queries. But I think I'll actually leave this out for now and add it in later if it becomes necessary. Even if we do keep this kind of a key, it might make more sense in a separate model that specifically separates the publishing details (published_version
, first_published_at
, last_published_at
, etc.).
The item model hiearachy is: Item -> ItemVersion -> ItemInfo -> ItemRaw | ||
|
||
Item is the versionless thing that is guaranteed to exist for the lifetime of | ||
the LearningContext. An ItemVersion is a different version of that item for a | ||
given LearningContext, and may include policy changes (like grading). ItemInfo | ||
covers basic metadata that is intrinsic to the item itself, and now how it's | ||
used in a LearningContext. ItemRaw represents the raw byte data. | ||
|
||
TODO: Add link to ADR after it merges. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack, sorry, I am struggling to rebuild this hierarchy in my head. I'm going to try to rephrase it here. Let me know if I'm off:
- ...Assume "LC" is some LearningContext, and "LCV" some LearningContextVersion within LC.
- An ItemRaw is a blob of byte data and a mimetype.
- Example: an ItemRaw instance "PR", containing some OLX file
- An ItemInfo associates an ItemRaw with metadata, including its LearningContext and its item type.
- Example: an ItemInfo instance "PI", which tells us that PR is a
problem
and belongs to LC.
- Example: an ItemInfo instance "PI", which tells us that PR is a
- An Item is an atomic block of content associated with an LC. In isolation, an Item does not tell us about its content, because content is associated with versions, whereas an Item is versionless.
- Example: an Item instance "P" in LC.
- An ItemVersion joins together an abstract Item instance with a concrete ItemInfo, denoting a particular version of the content, policy, and metadata of an Item.
- Example: an ItemVersion instance "PV", joining together P and PI.
- Example: an ItemVersion instance "PV2", joining together P and PI, with some updated policy.
- Example: a later ItemVersion instance "PVx", joining together P and some modified ItemInfo "PIx".
- and so on
- LearningContextVersionItemVersions relate LearningContextVersions to ItemVersions. This table is the connective tissue telling us which ItemVersions are in a LearningContextVersion (and, by corollary, which Items are in a LearningContext).
- Example: PV is in LCV
- Example: PV2 is in "LCV2", a later version of LC
- Example: PV2 is also in "LCV3", a later version of LC that updates some Item(s) other than P
""" | ||
uuid = immutable_uuid_field() | ||
learning_context = models.ForeignKey(LearningContext, on_delete=models.CASCADE) | ||
type = models.CharField(max_length=100) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we always deduce type
from item_raw.mime_type
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had originally meant it as something different, but I'm not sure if that holds up in the latest iteration.
The difference I had in mind originally was something like an HTMLBlock's HTML. That it would have an ItemInfo
that showed it was an XBlock with HTML handler, but the raw data had a MIME type of "text/html".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha. So I guess it depends whether we want to stuff OLX into the item_raws
of xblock-typed ItemInfos, or something smarter like the constituent HTML / Loncapa XML / image bytes.
The other fields on ItemRaw are for data that is intrinsic to the file data | ||
itself (e.g. the size). Any smart parsing of the contents into more | ||
structured metadata should happen in other models that hang off of ItemInfo. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ormsbee I am struggling a bit with the distinction between ItemInfo and ItemRaw.
What would we lose by collapsing them into a single model? Is there something bad about structured metadata models hanging off of the same model that's storing the raw bytes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I originally created the idea of ItemInfo
(under a different name), when I was thinking it would have more stuff in it (like a local identifier associated with the content package), but those concepts were simplified out of the design. We would have to be careful to not pull back the raw payload all the time, but that's manageable.
The only remaining use case I can think of is when you want to add a little bit of metadata about the content that is not the content itself–new information entered by a human, not something that's being derived by the code from an ItemRaw
. At that point, we have to figure out where to hang that data in a way that also clearly signals that this content has changed.
As an example, let's say we have a text description of an image.
If we attach that data to a model that hangs off of ItemRaw
, then there's no indication that the content has been updated. None of the versioning mechanisms above this were invoked or needed to be invoked. So we've done an unexpected mutation, and it will invalidate assumptions around versioning and caching. We can't even create a new ItemRaw
to bind it to (wasteful as that would be in terms of space), because ItemRaw
uses a hash of its contents for identity.
We could attach that data to a model that hangs off of ItemVersion
. That's a little weird because that's not policy data that changes from course to course, but we would have default policy information that's attached at that layer and copied, so maybe it would work out...?
You're right in that other comment, it's very much like the definition/usage split in the old system. But maybe we should take that as a cautionary tale–people put many things in the settings scope that should have been put in definitions. If the distinction of when to use ItemInfo
is unclear to you as an expert in this kind of system, then it will almost certainly confuse others who try to use this later. Maybe it's easier to say: "If you can completely derive it from the raw data, hang a model off of ItemRaw
, and if it's any new data entered in by a user, hang it off of ItemVersion
."?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realized I need to check an assumption about how I've been reading this: "hanging model X off of model Y" means that X would have a foreign key to Y, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realized I need to check an assumption about how I've been reading this: "hanging model X off of model Y" means that X would have a foreign key to Y, right?
Yes. I should have been clearer. 😄
FYI @bradenmacdonald: This is the data models PR I briefly mentioned in our meeting today. |
Hey @ormsbee , are you looking for another round of review on this one? |
@kdmccormick: Almost. I've been doing bits and pieces in the odd-hours for this. I need to update the import script to make sure these ideas all hold together and then it'll be ready for review. Though I don't expect the models.py files to change very much at this point, so if you want to take another pass at that, you're more than welcome to. |
@kdmccormick: ready for another round of review, with the caveat that I'd ask that you largely ignore |
|
||
The idea is that if you have a model that is associated with a specific | ||
version of an item, the join is going to be 1:1 with an ItemVersion, and | ||
potentially M:1 with your data model. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for some instance of this, say ItemVersionData -> Data
:
- is
Data
immutable? - is
ItemVersionData:Data
an M:1 relationship because multiple versions of an Item may reference the same underlying Data?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is Data immutable
It should be or Bad Things will happen.
is ItemVersionData:Data an M:1 relationship because multiple versions of an Item may reference the same underlying Data?
Yes. One of the underlying assumptions here is that if you have some data that's associated with an ItemVersion, changing that data means that the Item has changed in some way, and thus forms a new ItemVersion. So if you change the retry-settings or some such for an Item, there's a new ItemVersion. But for all the other apps that didn't change their data, we can just make new entries that join the new ItemVersion with the same data that those apps had for the last ItemVersion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be or Bad Things will happen.
Cool, I assumed so but wanted to be sure. Is there a way to indicate that, like marking it read-only in Django admin?
Yes. ...
Makes sense 👍🏻 It would be good to include that explanation somewhere in the library before releasing v1.0. Not a blocker for this PR; I know you're still iterating.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, I assumed so but wanted to be sure. Is there a way to indicate that, like marking it read-only in Django admin?
Yeah, I'll definitely do that when I start making some admin pages.
# TODO: maybe we should split it up into two fields instead of doing | ||
# substring searches? | ||
mime_type = models.CharField(max_length=255, blank=False, null=False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we should split it up into two fields instead of doing substring searches?
Sounds like a win to me. Any downsides?
Another reason to split: MIME types can include a parameter
field, eg text/plain;charset=utf8
. If you don't want to support the parameter field, then splitting into two fields would make that more explicit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I'm just used to thinking them as a combined thing because I can't really see querying one in isolation from the other. But it is more normalized and it's easy to add, so 🤷 I'll add it.
Minimal abstract model to let people attach data to ItemVersions. | ||
|
||
The idea is that if you have a model that is associated with a specific | ||
version of an item, the join is going to be 1:1 with an ItemVersion, and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, I'm reading that:
- each ItemVersion can only be associated with at most one of each type of "Data" model (Image, Asset, Video, etc.), but
- each ItemVersion may be associated with an arbitrary number of Content objects.
This confused me at first, because the "Data" models and the Content model occupy very similar roles in my head. But I think I'm beginning to understand it... is this a good example?
- Some ItemVersion represents a video.
- It has multiple Content items associated with: one that points to the video file, one PNG thumbnail, and several others text files that are transcripts.
- In the staticassets app, it has a single associated Video instance and Asset instance. Video metadata as well as information about the transcripts are part of (or hang off) of the Video model.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that was my intent. A couple of extra notes about it:
Content survives after apps die.
At some point, an app becomes unsupported and disappears. At that point, all the Item/ItemVersion-related data it has dies as well. But Content is associated with the ItemVersion directly in itemstore
, so that raw data will still export.
So I think this encourages a pattern where we store raw things in Content and then build smarter relational data structures out of them connected to ItemVersion. For instance, a ProblemBlock's raw OLX can live in Content while richer relational tables live in a new ProblemBlock app that give a richer, more queryable interface.
ItemVersion mapping to just one row in a "Data" model is common, but not required.
I think that most of the time apps want to associate one piece of data with a given ItemVersion (e.g. a problem weight, schedule, etc.), but that's not a requirement. A rich data model of a ProblemBlock might want to create entries for each response, for instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ItemVersion mapping to just one row in a "Data" model is common, but not required.
How would that look?
For example, imagine we have an ItemVersion representing a problem block, and Response data models that we want to (indirectly) hang off of it. Would it look like:
ItemVersion <--(1:1)-- ItemVersionProblemBlock --(N:N)--> Response
or:
ItemVersion <--(1:N)-- ItemVersionResponse --(N:1)--> Response
or something else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I thought it would be like:
ItemVersion <-- (N:1) -- ItemVersionProblemBlock -- (N:N) --> Response
That would still allow some central process to increment to the next version in a simple way without understanding what ItemVersionProblemBlock is. But I honestly haven't thought it through that much.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of questions but nothing blocking this from landing. The re-organization generally makes sense to me based on previous conversations.
|
||
Each ItemVersion belongs to one and only one Item. | ||
|
||
TODO: created_by field? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to confirm, I would see this as something that would be useful for debugging and analyzing but not as a thing that would bubble out to an end user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that was my thought as well.
# Question: Title is the only thing here that actually has human-readable | ||
# text. Does it make sense to lift it out into a separate metadata model, | ||
# possibly even one with language awareness? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think maybe this is a low level enough thing that we don't need to do that. I'm thinking about how the contents if they're XBlocks for example, will probably have a specific language and so maybe it makes more sense to build language awareness at a higher level?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think you're right that it belongs in a higher level. In any case, I'm definitely fine with punting this for now, even if we do bake it at this layer later. I'll remove the comment.
but changing it will affect all ItemVersions. | ||
""" | ||
uuid = immutable_uuid_field() | ||
learning_context = models.ForeignKey(LearningContext, on_delete=models.CASCADE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So an item can only be in one LearningContext
(LC), but since an ItemVersion
can be in any number of LearningContextVersions
(LCV) do we need a constraint somewhere that ensures that all those LCVs are from the same LC? Or is that too paranoid at this stage?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like being paranoid about the data model in general, but I'm not sure what we can do with MySQL here, or how much overhead adding CheckConstraints is going to cause. I'll see how hard it is.
# TODO: maybe we should split it up into two fields instead of doing | ||
# substring searches? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When would we need to do a partial search? Is it just when looking at types and ignoring sub-types? or something more complex?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's the only situation I can really think of.
This splits Item-related models into a new app and otherwise follows the logical split discussed here.