Skip to content

ADR for shared Assets and how course files are referenced/served from openedx_content [FC-0138] - #822

Merged
bradenmacdonald merged 45 commits into
mainfrom
braden/serving-assets
Oct 30, 2026
Merged

bradenmacdonald merged 45 commits into
mainfrom
braden/serving-assets

Conversation

@bradenmacdonald

@bradenmacdonald bradenmacdonald commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

This is an ADR for representing Course Files in openedx_content.

Builds on #812 and #818 (merged).

View the rendered ADRs here:

Private ref: FAL-4383


AI disclosure: Claude helped me with research, writing, and formatting the RST, but I wrote most of the content myself.

@openedx-webhooks openedx-webhooks added open-source-contribution PR author is not from Axim or 2U core contributor PR author is a Core Contributor (who may or may not have write access to this repo). labels Sep 19, 2026
@openedx-webhooks

openedx-webhooks commented Sep 19, 2026 •

Copy link
Copy Markdown

Thanks for the pull request, @bradenmacdonald!

This repository is currently maintained by @axim-engineering.

Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@mphilbrick211 mphilbrick211 moved this from Needs Triage to Waiting on Author in Contributions Sep 22, 2026
@mphilbrick211 mphilbrick211 added the FC Relates to an Axim Funded Contribution project label Sep 22, 2026
@bradenmacdonald
bradenmacdonald force-pushed the braden/serving-assets branch 2 times, most recently from 6fe58fa to ee4a005 Compare September 22, 2026 23:47
@bradenmacdonald
bradenmacdonald marked this pull request as ready for review September 22, 2026 23:48
@bradenmacdonald
bradenmacdonald marked this pull request as draft September 23, 2026 21:03
@bradenmacdonald bradenmacdonald changed the title ADR for reusable asset "Uploads" and how course assets are referenced/served [FC-0138]- #812 ADR for reusable "File components" and how course assets are referenced/served [FC-0138] Sep 23, 2026
@bradenmacdonald

Copy link
Copy Markdown
Contributor Author

For those following this so far, I have made major revisions to everything, primarily renaming it from "Upload" to "File component" and adding a "Consequences" section.

Naturally, the ``FileComponentMetadata`` table will enforce that ``legacy_path`` is unique per learning package.

2. File components can be referenced using new ``oex-asset:`` URL
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just pushed this new ADR 14.

Decision 1 is the only part that is relevant to our short-term project of "move static assets into openedx_content and serve them using the contentstore API". The rest is trying to paint a picture of the long-term functionality we'll get when we also migrate courseware itself to openedx_content.

The reason I'm sketching this all out now is so we can decide if we want all this, or if we'll instead go with the dumb, simple approach that does the bare minimum for contentstore compatibility but drops versioning, tracking, linking, etc.

@bradenmacdonald
bradenmacdonald marked this pull request as ready for review September 26, 2026 01:10
@bradenmacdonald

Copy link
Copy Markdown
Contributor Author

OK, I still have some TODOs and things to clean up, but I'd like to get more input before focusing on the fine details. This is ready for review, @ChrisChV @ormsbee @kdmccormick.

CC @xitij2000 and @Kelketek on ADR 14 in case you have any thoughts on this after working on the XBlock Asset Picker UI.

When copying a Component with references to shared File components to a new Learning Package, each referenced File component is handled as follows:

- First, if the destination Learning Package already has a File component that is a downstream copy of the same upstream File component (see decision 6), that existing copy is reused, even if its ``component_code`` or content differs. The copy is not updated as a side effect of pasting, since that would change every other component that uses it; if the source uses a newer upstream version, the author can sync the File component as usual. Matching on upstream prevents repeated pastes or imports of the same library content from creating a new copy of the File component each time.
- Otherwise, if the destination Learning Package already has a File component with identical ``component_code`` and media asset file hash(es), it is reused and no File components need to be copied. Identical bytes don't prove that it's the same asset, but reusing a File component with the same code and content is harmless.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit worried about this one. If this occurs, the File Component in the destination Learning Package might already be in use by another component. If an author updates the asset, it will be updated in both the existing component and the copied component. However, the author is unaware of this; as far as they are concerned, they are only changing the asset in the existing component.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but we will presumably have something in the UI that shows authors where each File component is being used, so they would see a list of which changes will happen when they update it.

There is actually something like that in the "Files" page already, but it's currently not 100% accurate.

11. Image metadata will be in separate models
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

As mentioned in the :class:`Media` docstring, we can use a separate model called ``ImageMedia`` to capture metadata like image dimensions (assuming it can be purely derived from the image byte data itself). We can also use a separate model like ``ImageComponentVersion`` to store *editable* metadata about an image, such as its default alt text and whether or not the image is purely decorative; saving edits of such metadata would create a new version of the File component.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that a File component that bundles an image with its thumbnail will have separate ImageMedia entries for each thumbnail size and the original asset?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't think thumbnails should be considered part of the asset, nor bundled with the image. But if you did upload a pair of images with a regular and "retina" @2x resolution versions, each of those would have its own ImageMedia entry. The ImageMedia is 1:1 with each actual image file that we store (de-duplicated by hash), so it doesn't matter how they are logically organized. (Two different-resolution versions in one component or two separate components each with a different resolution.)

As for how thumbnails are stored, see the draft proposal from @ChrisChV at #837 which is more or less a separate system for storing thumbnails, not storing them as versioned file components.


This is an appealing alternative, especially when you consider that the number of use cases that require multiple files per File component is likely very small. Videos require multiple files but are rarely if ever stored in Course Files (instead hosted on video-specific services like YouTube or edX.org's video platform). This is rejected primarily to support the use case of HTML "interactive" files that in turn reference image, JavaScript, and/or CSS files. Examples of this can be seen in the `Studio Advanced course <https://github.com/HarvardX/studio-advanced/tree/5cf820c29e3439c9f45cb88e5cf22fd4ab269739/course/static>`_.

TODO: can we quantify how much this feature would be used?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think images are probably a big user of this and I expect would be very common as assets. I think embracing this would allow uploading the original asset and using a web-friendly resized version in a course and potential a mobile version on mobile.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, for the PDF XBlock I'd worked on a feature to convert a .docx or other file type to pdf. It would be great that could be represented via this mechanism.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradenmacdonald I think we need to identify which assets are authored and which are derived/generated. This is because adding an asset to a File Component means a new version of that File Component will be created. That is correct for authored assets, but creating a new version for derived assets is not ideal. I’ve explored this further in the thumbnails ADR (#837). Thumbnails are derived/generated and disposable; it doesn't make much sense to version-control them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points guys. If we are auto-converting .docx to .pdf or something, I think that it should be treated differently than if an author uploads both a .docx and a .pdf.

Derived files, like thumbnails and auto-converted docs:

  • Are safe to delete
  • Can be (re-)created on demand
  • Are versioned differently: based on when the original file or the code that produces them changes (e.g. if we upgrade "pdflib" to a new version, we may want to re-create all the .pdf auto-conversions)

I think it makes sense to store such things in completely different models, not re-using the PublishableEntity machinery that we're using for the original file components themselves.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is another use-case for images, which is to allow editing while retaining the original. I think the versioning ability would probably be a better fit for that though. i.e. if a the authoring MFE or a plugin allows editing image it could save that as v2 of the asset or as another file in the same component.
Just pointing out a potential use case, not sure what would be the correct approach.

@kdmccormick kdmccormick left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the writeups! this is making the whole problem space much more concrete.

i'm still chewing on all three ADRs. here are just a couple specific concerns on ADR 14 I wanted to capture, but don't feel like you need to address these in depth right now as I'm still not sure about ADR 13

- First, if the destination Learning Package already has a File component that is a downstream copy of the same upstream File component (see decision 6), that existing copy is reused, even if its ``component_code`` or content differs. The copy is not updated as a side effect of pasting, since that would change every other component that uses it; if the source uses a newer upstream version, the author can sync the File component as usual. Matching on upstream prevents repeated pastes or imports of the same library content from creating a new copy of the File component each time.
- Otherwise, if the destination Learning Package already has a File component with identical ``component_code`` and media asset file hash(es), it is reused and no File components need to be copied. Identical bytes don't prove that it's the same asset, but reusing a File component with the same code and content is harmless.
- In both of the above cases, only the main Component needs to be copied, but we still have to create ``FileComponentLink`` and :class:`PublishableEntityVersionDependency` objects to track the relationship, using the original ``component_code`` as the ``alias`` if the reused File component's code differs.
- Otherwise, the File component is copied into the destination Learning Package, and becomes a downstream of the original if the original is in a library. If the destination Learning Package already has a conflicting File component, with identical ``component_code`` but different media asset file hash(es), the copy is given a different ``component_code``, and the ``FileComponentLink`` created for it will use the ``alias`` column to alias this new code to the old code, so that no changes to the OLX are required.

@kdmccormick kdmccormick Sep 30, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does the FileComponentLink's alias get factored in when the OLX is moved out of the target LP? notably:

  • copy
  • export/import

would each of those systems now need to read FileComponentLink in order to make sense of the OLX? is that tradeoff worth the benefit of not rewriting media paths upon OLX paste?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Export/import would presumably be at the LP level, and would need to include some list of all the FileComponentLinks in the package in any case.

Copy and paste into another LP would have to follow the same algorithm described above for handling references in the new learning package, creating an alias if needed.


This scheme:

- **Is easy to detect.** ``oex-asset:…`` does not occur in content for any other reason, so a single pattern finds every reference in any field type (HTML, problem XML, JSON, CSS ``url()``, etc.) with no false positives from platform or theme ``/static/`` URLs. (The only exception is that course content *about* OLX authoring itself would need to escape ``oex-asset:`` if it occurs in text.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine we'll need to support rewriting of /static/, basically forever. So, adding rewrite-detection for oex-asset: would not simplify the system, right?

Beyond that, it seems like all the other listed benefits of oex-asset: also apply to /static.

The net-new thing I see oex-asset: doing is that it provides for both the component-local format (oex-asset:/{path}) and the cross-component format (oex-asset:{component_alias}/{path}).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mentioned this in the meeting, but to recap here:

The problem with /static/ is that it can occur in many unrelated contexts, such as a course referencing https://www.python.org/static/img/python-logo.png or https://en.wikipedia.org/static/images/icons/wikipedia.png (although they shouldn't be hotlinking).

So we use this regular expression to try to only match quoted references that begin with /static/, like <img src="/static/..." .... But this doesn't match every use case; e.g. &quot;/static/...&quot; would not be matched, but could be a valid reference to a file.

Because oex-asset: doesn't occur in any other contexts (I'm pretty sure), we can detect it reliably and robustly.

@bradenmacdonald

Copy link
Copy Markdown
Contributor Author

@kdmccormick Thanks for the review! I have pushed some commits that should address everything you mentioned, plus what we discussed at the last meeting. I've also generally tried to simplify the ADRs and shorten them where possible, leaving some details to implementation or future ADRs.

CC @ChrisChV


Filenames should be case-sensitive, such that ``fig1.png`` and ``Fig1.png`` are both allowed. Since MySQL's default collation is case-insensitive, the ``filename`` column must explicitly use a case-sensitive collation (e.g. using ``case_sensitive_char_field``, as ``ref_field`` does), including for its uniqueness constraint. For backwards compatibility, the filename is also allowed to contain ``/``, such as ``images/image1.png``. By convention, an Asset's ``entity_ref`` is ``openedx-asset:{filename}``, but nothing (including asset URLs) should rely on that; look Assets up by ``filename`` using its explicit column.

If a file is uploaded with the same filename as an Asset that has been (soft) deleted, the deleted Asset is un-deleted: the upload becomes a new draft version of the existing Asset, rather than creating a new Asset. Its version history, flags and any existing references to it are retained.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When performing an "un-delete," is the value of allow_scripts retained? It would be good to specify that, for security reasons, this would be the only case in which it would revert to False.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, good point. I've tried to clarify this in 6c9fba7.

8. Course block usages will be tracked
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

In modulestore, courses are represented by an instance of the ``course`` XBlock, which forms the root of the course content tree. This XBlock has several references to shared asset files that can be specified, such as ``course_image``, ``banner_image``, ``video_thumbnail_image``, certificate signatory images, textbook PDF URLs, HTML textbook URLs, and instructor images. Such usages should continue working as they do now, but ideally be tracked as dependencies, with ``PublishableEntityVersionDependency``. The details of this are left to implementation, but may be achieved using :ref:`openedx-content-adr-0015` to shadow the root ``course`` block.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The details of this are left to implementation, but may be achieved using :ref:openedx-content-adr-0015 to shadow the root course block.

In ADR 15, it says it’s only for Leaf XBlocks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Good catch. This would be a new, second use for shadow components. I thought we could decide on that detail later, once we've learned a little from implementing this.

bradenmacdonald and others added 2 commits October 21, 2026 10:56
Co-authored-by: Chris Chávez <xnpiochv@gmail.com>

@kdmccormick kdmccormick left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I though this would be my final pass, but I ended up having one more idea I wanted your thoughts on.

Comment on lines +51 to +57
This is largely for backwards compatibility. Existing courses have ``.html`` files in their Files that load ``.js``, ``.css`` and image files from the same course by relative URL, and are framed or linked from course content. Plain (unscripted) HTML and SVG documents work under the ``sandbox`` policy of :ref:`openedx-content-adr-0005`; HTML that relies on running scripts additionally needs the ``allow_scripts`` flag (decision 8). For new content, HTML interactives are better implemented as an HTML Component with the related files attached to it: the interactive's markup lives in the component's own HTML, which loads the attached ``.js``, ``.css`` and image files as subresources.

To achieve this, Assets are served under their own URL form, defined in :ref:`openedx-content-adr-0005` ("URLs"), in which the filename is the final part of the path and all Assets of a learning package share one directory::

.../{context}/assets/{draft|published}/{filename}

For example, an HTML Asset served at ``.../assets/published/page.html`` that references ``image.png`` causes the browser to request ``.../assets/published/image.png``, which is the ``image.png`` Asset's own URL. Filenames containing ``/`` work the same way: ``images/diagram.html`` referencing ``../logo.png`` resolves to ``.../assets/published/logo.png``.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about rewriting all /static/ references as component-local URLs, and then having libraries refuse to serve shared assets for components which are not declared dependencies?

For example, /static/{file} within component@version would alway become:

  • https://.../{context}/.../{componsent}/{version}/assets/{file}

And HTTP requests to it would be resolved using the logic:

  • Check permissions on component, 404 or 403 if denied.
  • Prefer the local file within component@version if it exists.
  • Otherwise, check for a shared asset file of context. Then, either:
    • Lenient mode: Serve the file.
    • Strict mode (new): Serve the file iff there exists a PubEntDep exists between component and file.
  • Otherwise, 404.

Critically, this would mean that shared assets are served relative to the component, so a relative link from asset A to asset B would only resolve if B were also a declared component dependency.

We could start with libraries running in strict mode and existing courses running in lenient mode. The strictness could be ratcheted up over time if we wanted to move towards PubEntDeps being guaranteed. It would mean that, if we wanted to add UI support for shared assets in libraries, we'd need an explicit sort of "add asset to component" UX workflow, but I think that'd be a good thing.

Under this system, I think /static/ rewriting becomes simpler, because it doesn't care about whether its target asset it local or global. Operators would be able to observe which shared assets are being used by which components, whether by /static/ reference or by relative reference. Lastly, edits to relatively-linked library assets would show up as a changes on components rather than going undetected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to do something like that, but I was afraid it wasn't possible because of how we render pages one unit at a time instead of rendering each XBlock separately. However, now that I think about it more, I think that we still do static replacement on a block-by-block basis, so it should be possible. It might require updating some call sites like this that don't currently pass the block ID (so we can't get the component ID), and it won't cover cases like this that are outside of an XBlock context.

So yeah, I like that idea.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kdmccormick I incorporated this idea: 8d1222f

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @bradenmacdonald . Another benefit of this I see is that it will make it more likely that when you bring a component in from a library to a course, you also bring in all the shared assets it needs. And when one of those shared assets change in the library, it makes the component sync-able.

But I'm seeing this conflict with the proposed mechanism of re-computing PubEntVerDeps automatically from /static/ references whenever a draft is made. These relative assets references won't be detectable. So, I think we'll need to do one of 3 things:

  • Support explicit author-specified dependency relationships between shared Assets, with protection against dependency cycles.
  • Support explicit author-specified dependency relationships from Components to shared Assets (probably new model, ComponentAssetDependency), which, along with in-OLX /static/ references, are used as input to compute PubEntVerDeps.
  • Assert that relative asset referenced are simply not supported in shared library assets.

Do you agree? I don't think we need to implement that now, since this concern doesn't affect lenient contexts (courses) and there are no such thing as shared library Assets yet. But I think we should document in the ADR it'll be necessary in order to get shared library Assets working right.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would lean toward

Assert that relative asset referenced are simply not supported in shared library assets.

Because in libraries we don't have to worry about backwards compatibility, and supporting these relative references between static assets is a pain that only exists because a subset of courses has built HTML interactives that way. We can say that when building such things for libraries, component-local assets should be used exclusively.

@kdmccormick kdmccormick left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still working on a final pass; I should have the round of review done by tomorrow morning

Co-authored-by: Kyle D McCormick <kyle@axim.org>

@kdmccormick kdmccormick left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This plan looks great and I'm very excited to move forward with it. These last few comments are just wording suggestions; take or leave as much of them as you like, no need for another review pass from me.

Thank you! 🚀

Comment on lines 96 to +120
The ``version`` can be:

* ``draft`` indicating the latest draft version (viewed by authors in Studio).
* ``published`` indicating the latest published version (viewed by students in the LMS)
* ``v{num}`` meaning a specific version–e.g. ``v20`` for version 20.
* If the ``version`` is ``draft``, then ``?v={num}`` can also be used to retrieve a specific version - e.g. ``?v=20`` for version 20. For shared Assets, this is the version of the requested Asset only, and the response is always sent as an attachment (see :ref:`openedx-content-adr-0013`, decision 5).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the version rules apply to component asset URLs too, or just shared asset URLs?

If the former, I'd say "For both URL formats, the version can be ..."

If the latter, I would group this paragraph with the shared-asset-URL description by moving it above "Shared Assets that are used by a Component..."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll just use it for shared asset URLs (not Shared Assets that are used by a Component) for now and see how it goes. We can decide during implementation if we have a need for the version spec in the latter case, but I suspect we'll be fine without it.

@bradenmacdonald

Copy link
Copy Markdown
Contributor Author

Thanks for working through these large ADRs and for the great reviews!

@bradenmacdonald
bradenmacdonald merged commit 11c0ed1 into main Oct 30, 2026
7 checks passed
@bradenmacdonald
bradenmacdonald deleted the braden/serving-assets branch October 30, 2026 22:21
@github-project-automation github-project-automation Bot moved this from Waiting on Author to Done in Contributions Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core contributor PR author is a Core Contributor (who may or may not have write access to this repo). FC Relates to an Axim Funded Contribution project open-source-contribution PR author is not from Axim or 2U

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants