diff --git a/hypha/public/projects/management/commands/migrate_projects.py b/hypha/public/projects/management/commands/migrate_projects.py
deleted file mode 100644
index 28eabd427ff365c35054257ad5e1d925fffa9de5..0000000000000000000000000000000000000000
--- a/hypha/public/projects/management/commands/migrate_projects.py
+++ /dev/null
@@ -1,296 +0,0 @@
-import argparse
-import itertools
-import json
-import mimetypes
-from datetime import datetime, timezone
-from io import BytesIO
-from urllib.parse import urlsplit
-
-import bleach
-import requests
-from django.core.files.images import ImageFile
-from django.core.management.base import BaseCommand
-from django.db import transaction
-from django.db.utils import IntegrityError
-from PIL import Image
-from wagtail.admin.rich_text.converters.editor_html import EditorHTMLConverter
-from wagtail.images import get_image_model
-from wagtail.models import Page
-from wagtail.rich_text import RichText
-
-from hypha.apply.categories.categories_seed import CATEGORIES
-from hypha.apply.categories.models import Category, Option
-from hypha.public.projects.models import (
-    ProjectContactDetails,
-    ProjectFunding,
-    ProjectIndexPage,
-    ProjectPage,
-)
-
-WagtailImage = get_image_model()
-
-VALID_IMAGE_EXTENSIONS = [
-    ".jpg",
-    ".jpeg",
-    ".png",
-    ".gif",
-]
-
-VALID_IMAGE_MIMETYPES = ["image"]
-
-
-def valid_url_extension(url, extension_list=VALID_IMAGE_EXTENSIONS):
-    return any(url.endswith(e) for e in extension_list)
-
-
-def valid_url_mimetype(url, mimetype_list=VALID_IMAGE_MIMETYPES):
-    mimetype, encoding = mimetypes.guess_type(url)
-    if mimetype:
-        return any(mimetype.startswith(m) for m in mimetype_list)
-    else:
-        return False
-
-
-class Command(BaseCommand):
-    help = "Project migration script. Requires a source JSON file."
-    data = []
-    terms = {}
-    whitelister = EditorHTMLConverter().whitelister
-
-    def add_arguments(self, parser):
-        parser.add_argument(
-            "source", type=argparse.FileType("r"), help="Migration source JSON file"
-        )
-
-    @transaction.atomic
-    def handle(self, *args, **options):
-        # Prepare the list of categories.
-        for item in CATEGORIES:
-            category, _ = Category.objects.get_or_create(name=item["category"])
-            option, _ = Option.objects.get_or_create(
-                value=item["name"], category=category
-            )
-            self.terms[item["tid"]] = option
-
-        self.parent_page = ProjectIndexPage.objects.first()
-
-        if not self.parent_page:
-            raise ProjectIndexPage.DoesNotExist(
-                "Project Index Page must exist to import projects"
-            )
-
-        self.funds = {
-            "3625": Page.objects.get(title="Internet Freedom Fund"),
-            "3654": Page.objects.get(title="Rapid Response Fund"),
-            "3905": Page.objects.get(title="Core Infrastructure Fund"),
-            "7791": Page.objects.get(title="Community Lab"),
-        }
-
-        with options["source"] as json_data:
-            self.data = json.load(json_data)
-
-            counter = 0
-            for id in self.data:
-                self.process(id)
-                counter += 1
-
-            self.stdout.write(f"Imported {counter} submissions.")
-
-    def process(self, id):
-        node = self.data[id]
-
-        try:
-            project = ProjectPage.objects.get(drupal_id=node["nid"])
-        except ProjectPage.DoesNotExist:
-            project = ProjectPage(drupal_id=node["nid"])
-
-        # TODO timezone?
-        project.submit_time = datetime.fromtimestamp(int(node["created"]), timezone.utc)
-
-        project.title = node["title"]
-
-        image_url_base = "https://www.opentech.fund/sites/default/files/"
-        try:
-            uri = node["field_project_image"]["uri"]
-        except TypeError:
-            # There was no image
-            pass
-        else:
-            parts = urlsplit(uri)
-            image_url = image_url_base + parts.netloc + parts.path
-            project.icon = self.wagtail_image_obj_from_url(
-                image_url, node["field_project_image"]["fid"]
-            )
-
-        project.introduction = self.get_field(node, "field_preamble")
-
-        cleaned_body = self.whitelister.clean(self.get_field(node, "body"))
-        if project.introduction:
-            project.body = [("paragraph", RichText(cleaned_body))]
-        else:
-            # Use the first sentence of the body as an intro
-            very_clean_body = bleach.clean(cleaned_body, strip=True)
-            introduction = very_clean_body.split(".")[0] + "."
-            project.introduction = introduction
-            body_without_intro = cleaned_body.replace(introduction, "").strip()
-            project.body = [("paragraph", RichText(body_without_intro))]
-
-        status = {
-            "329": "idea",
-            "328": "exists",
-            "366": "release",
-            "367": "production",
-        }
-        project.status = status[node["field_proposal_status"]["tid"]]
-
-        project.contact_details.clear()
-
-        sites = node["field_project_url"]
-
-        if isinstance(sites, dict):
-            sites = [sites]
-
-        for site in sites:
-            url = site["url"]
-            if "github" in url:
-                page_type = "github"
-                url = urlsplit(url).path
-            else:
-                page_type = "website"
-
-            project.contact_details.add(
-                ProjectContactDetails(
-                    service=page_type,
-                    value=url,
-                )
-            )
-
-        project.contact_details.add(
-            ProjectContactDetails(
-                service="twitter", value=self.get_field(node, "field_project_twitter")
-            )
-        )
-
-        # Funding
-        project.funding.clear()
-
-        years = self.ensure_iterable(node["field_project_funding_year"])
-        amounts = self.ensure_iterable(node["field_project_funding_amount"])
-        durations = self.ensure_iterable(node["field_project_term_time"])
-        funds = self.ensure_iterable(node["field_project_funding_request"])
-        for year, amount, duration, fund in itertools.zip_longest(
-            years, amounts, durations, funds
-        ):
-            try:
-                fund = self.funds[fund["target_id"]]
-            except TypeError:
-                fund = None
-
-            try:
-                duration = duration["value"]
-            except TypeError:
-                duration = 0
-
-            try:
-                amount = amount["value"]
-            except TypeError:
-                # This is an error, don't give funding
-                continue
-
-            project.funding.add(
-                ProjectFunding(
-                    value=amount,
-                    year=year["value"],
-                    duration=duration,
-                    source=fund,
-                )
-            )
-
-        category_fields = [
-            "field_term_region",
-            "field_term_country",
-            "field_technology_attribute",
-            "field_proposal_theme",
-            "field_proposal_focus",
-            "field_proposal_beneficiaries",
-        ]
-        categories = {}
-        for category in category_fields:
-            terms = self.ensure_iterable(node[category])
-            for term in terms:
-                option = self.get_referenced_term(term["tid"])
-                if option:
-                    categories.setdefault(option.category.id, []).append(option.id)
-
-        project.categories = json.dumps(categories)
-
-        try:
-            if not project.get_parent():
-                self.parent_page.add_child(instance=project)
-            project.save_revision().publish()
-            self.stdout.write(
-                f"Processed \"{node['title'].encode('utf8')}\" ({node['nid']})"
-            )
-        except IntegrityError:
-            self.stdout.write(
-                f"*** Skipped \"{node['title']}\" ({node['nid']}) due to IntegrityError"
-            )
-            pass
-
-    def ensure_iterable(self, value):
-        if isinstance(value, dict):
-            value = [value]
-        return value
-
-    def get_field(self, node, field):
-        try:
-            return node[field]["safe_value"]
-        except TypeError:
-            pass
-        try:
-            return node[field]["value"]
-        except TypeError:
-            return ""
-
-    def get_referenced_term(self, tid):
-        try:
-            return self.terms[tid]
-        except KeyError:
-            return None
-
-    def nl2br(self, value):
-        return value.replace("\r\n", "<br>\n")
-
-    @staticmethod
-    def wagtail_image_obj_from_url(url, drupal_id=None):
-        """
-        Get the image from the Nesta site if it doesn't already exist.
-        """
-
-        if drupal_id is not None and drupal_id:
-            try:
-                return WagtailImage.objects.get(drupal_id=drupal_id)
-            except WagtailImage.DoesNotExist:
-                pass
-
-        if url and valid_url_extension(url) and valid_url_mimetype(url):
-            r = requests.get(url, stream=True)
-
-            if r.status_code == requests.codes.ok:
-                img_buffer = BytesIO(r.content)
-                img_filename = url.rsplit("/", 1)[1]
-
-                # Test downloaded file is valid image file
-                try:
-                    pil_image = Image.open(img_buffer)
-                    pil_image.verify()
-                except Exception as e:
-                    print(f"Invalid image {url}: {e}")
-                else:
-                    img = WagtailImage.objects.create(
-                        title=img_filename,
-                        file=ImageFile(img_buffer, name=img_filename),
-                        drupal_id=drupal_id,
-                    )
-                    return img
-        return None
diff --git a/hypha/public/projects/migrations/0012_remove_projectfunding_page_and_more.py b/hypha/public/projects/migrations/0012_remove_projectfunding_page_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..b2751cbdcd3eeaa4afa47ad57235f76d3eb378b9
--- /dev/null
+++ b/hypha/public/projects/migrations/0012_remove_projectfunding_page_and_more.py
@@ -0,0 +1,71 @@
+# Generated by Django 4.2.9 on 2024-01-10 08:34
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("projects", "0011_remove_projectindexpage_social_image_and_more"),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name="projectfunding",
+            name="page",
+        ),
+        migrations.RemoveField(
+            model_name="projectfunding",
+            name="source",
+        ),
+        migrations.RemoveField(
+            model_name="projectindexpage",
+            name="header_image",
+        ),
+        migrations.RemoveField(
+            model_name="projectindexpage",
+            name="listing_image",
+        ),
+        migrations.RemoveField(
+            model_name="projectindexpage",
+            name="page_ptr",
+        ),
+        migrations.RemoveField(
+            model_name="projectpage",
+            name="header_image",
+        ),
+        migrations.RemoveField(
+            model_name="projectpage",
+            name="icon",
+        ),
+        migrations.RemoveField(
+            model_name="projectpage",
+            name="listing_image",
+        ),
+        migrations.RemoveField(
+            model_name="projectpage",
+            name="page_ptr",
+        ),
+        migrations.RemoveField(
+            model_name="projectpagerelatedpage",
+            name="page",
+        ),
+        migrations.RemoveField(
+            model_name="projectpagerelatedpage",
+            name="source_page",
+        ),
+        migrations.DeleteModel(
+            name="ProjectContactDetails",
+        ),
+        migrations.DeleteModel(
+            name="ProjectFunding",
+        ),
+        migrations.DeleteModel(
+            name="ProjectIndexPage",
+        ),
+        migrations.DeleteModel(
+            name="ProjectPage",
+        ),
+        migrations.DeleteModel(
+            name="ProjectPageRelatedPage",
+        ),
+    ]
diff --git a/hypha/public/projects/models.py b/hypha/public/projects/models.py
index bc6e704dd015f18258b0e85218ce8f576558cf11..173d99cc1b7464c5301c310abab30ca08f6cae2a 100644
--- a/hypha/public/projects/models.py
+++ b/hypha/public/projects/models.py
@@ -1,191 +1,2 @@
-import json
-
-from django.conf import settings
-from django.core.exceptions import ValidationError
-from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
-from django.core.validators import URLValidator
-from django.db import models
-from django.utils.translation import gettext_lazy as _
-from modelcluster.fields import ParentalKey
-from pagedown.widgets import PagedownWidget
-from wagtail.admin.panels import (
-    FieldPanel,
-    InlinePanel,
-    MultiFieldPanel,
-    PageChooserPanel,
-)
-from wagtail.fields import StreamField
-from wagtail.search import index
-
-from hypha.apply.categories.models import Option
-from hypha.public.utils.blocks import StoryBlock
-from hypha.public.utils.models import BaseFunding, BasePage, FundingMixin, RelatedPage
-
-from .widgets import CategoriesWidget
-
-
-class ProjectContactDetails(models.Model):
-    project_page = ParentalKey("ProjectPage", related_name="contact_details")
-    site_titles = (
-        ("website", "Main Website URL"),
-        ("twitter", "Twitter Handle"),
-        ("github", "Github Organisation or Project"),
-    )
-    site_urls = (
-        ("website", ""),
-        ("twitter", "https://twitter.com/"),
-        ("github", "https://github.com/"),
-    )
-    service = models.CharField(
-        max_length=200,
-        choices=site_titles,
-    )
-    value = models.CharField(max_length=255)
-
-    @property
-    def url(self):
-        return dict(self.site_urls)[self.service] + self.value
-
-    def service_name(self):
-        site_display = {
-            "twitter": "@" + self.value,
-            "github": "Github",
-            "website": "Main Website",
-        }
-        return site_display[self.service]
-
-    def clean(self):
-        if self.service == "twitter" and self.value.startswith("@"):
-            self.username = self.username[1:]
-
-        if self.service == "website":
-            validate = URLValidator()
-            try:
-                validate(self.value)
-            except ValidationError as e:
-                raise ValidationError({"value": e}) from e
-
-
-class ProjectPageRelatedPage(RelatedPage):
-    source_page = ParentalKey("ProjectPage", related_name="related_pages")
-
-    panels = [
-        PageChooserPanel("page", "projects.ProjectPage"),
-    ]
-
-
-class ProjectFundingQueryset(models.QuerySet):
-    def projects(self):
-        return (
-            ProjectPage.objects.filter(id__in=self.values_list("page__id"))
-            .live()
-            .public()
-        )
-
-
-class ProjectFunding(BaseFunding):
-    page = ParentalKey("ProjectPage", related_name="funding")
-
-    objects = ProjectFundingQueryset.as_manager()
-
-
-class ProjectPage(FundingMixin, BasePage):
-    STATUSES = (
-        ("idea", "Just an Idea. (Pre-alpha)"),
-        ("exists", "It Exists! (Alpha/Beta)"),
-        ("release", "It's basically done. (Release)"),
-        ("production", "People Use it. (Production)"),
-    )
-
-    subpage_types = []
-    parent_page_types = ["ProjectIndexPage"]
-
-    drupal_id = models.IntegerField(null=True, blank=True, editable=False)
-
-    introduction = models.TextField(blank=True)
-    icon = models.ForeignKey(
-        "images.CustomImage",
-        null=True,
-        blank=True,
-        related_name="+",
-        on_delete=models.SET_NULL,
-    )
-    status = models.CharField(choices=STATUSES, max_length=25, default=STATUSES[0][0])
-    body = StreamField(StoryBlock(), use_json_field=True)
-
-    categories = models.TextField(default="{}", blank=True)
-
-    wagtail_reference_index_ignore = True
-
-    search_fields = BasePage.search_fields + [
-        index.SearchField("introduction"),
-        index.SearchField("body"),
-    ]
-
-    content_panels = (
-        BasePage.content_panels
-        + [
-            FieldPanel("icon"),
-            FieldPanel("status"),
-            FieldPanel("introduction"),
-            FieldPanel("body"),
-            InlinePanel("contact_details", label=_("Contact Details")),
-        ]
-        + FundingMixin.content_panels
-        + [
-            InlinePanel("related_pages", label=_("Related Projects")),
-            MultiFieldPanel(
-                [FieldPanel("categories", widget=CategoriesWidget)],
-                heading=_("Categories"),
-                classname="collapsible collapsed",
-            ),
-        ]
-    )
-
-    def category_options(self):
-        categories = json.loads(self.categories)
-        options = [int(id) for options in categories.values() for id in options]
-        return (
-            Option.objects.select_related()
-            .filter(id__in=options)
-            .order_by("category_id", "sort_order")
-        )
-
-
-class ProjectIndexPage(BasePage):
-    subpage_types = ["ProjectPage"]
-    parent_page_types = ["home.Homepage", "standardpages.IndexPage"]
-
-    introduction = models.TextField(blank=True)
-
-    content_panels = BasePage.content_panels + [
-        FieldPanel("introduction", widget=PagedownWidget()),
-    ]
-
-    search_fields = BasePage.search_fields + [
-        index.SearchField("introduction"),
-    ]
-
-    def get_context(self, request, *args, **kwargs):
-        context = super().get_context(request, *args, **kwargs)
-        subpages = (
-            ProjectPage.objects.descendant_of(self)
-            .live()
-            .public()
-            .select_related("icon")
-            .order_by("-first_published_at")
-        )
-        per_page = settings.DEFAULT_PER_PAGE
-        page_number = request.GET.get("page")
-        paginator = Paginator(subpages, per_page)
-
-        try:
-            subpages = paginator.page(page_number)
-        except PageNotAnInteger:
-            subpages = paginator.page(1)
-        except EmptyPage:
-            subpages = paginator.page(paginator.num_pages)
-
-        context["subpages"] = subpages
-
-        return context
+# @TODO: This file is kept to generate delete migrations, this file should be removed, while removing the app from
+# INSTALLED_APPS and removing the migrations folder.
diff --git a/hypha/public/projects/templates/projects/includes/project_status.html b/hypha/public/projects/templates/projects/includes/project_status.html
deleted file mode 100644
index 1cf8615d259e510e7a0780878164a975eee8a410..0000000000000000000000000000000000000000
--- a/hypha/public/projects/templates/projects/includes/project_status.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<div class="wrapper--outer-space-medium">
-    <div class="wrapper wrapper--breakout wrapper--light-grey-bg wrapper--inner-space-medium">
-        <h3 class="heading heading--center heading--no-margin">Current project status</h3>
-        <div class="wrapper wrapper--large wrapper--status">
-            <div class="list list--status {% if page.status == 'idea' %}is-active{% endif %}">
-                <svg class="icon icon--status"><use xlink:href="#bulb"></use></svg>
-                <h5>Just an idea (Pre-alpha)</h5>
-            </div>
-            <div class="list list--status {% if page.status == 'exists' %}is-active{% endif %}">
-                <svg class="icon icon--status"><use xlink:href="#tick"></use></svg>
-                <h5>It exists! (Alpha/Beta)</h5>
-            </div>
-            <div class="list list--status {% if page.status == 'release' %}is-active{% endif %}">
-                <svg class="icon icon--status"><use xlink:href="#flags"></use></svg>
-                <h5>It's basically done (Release)</h5>
-            </div>
-            <div class="list list--status {% if page.status == 'production' %}is-active{% endif %}">
-                <svg class="icon icon--status"><use xlink:href="#tap-phone"></use></svg>
-                <h5>People use it (Production)</h5>
-            </div>
-        </div>
-    </div>
-</div>
diff --git a/hypha/public/projects/templates/projects/project_index_page.html b/hypha/public/projects/templates/projects/project_index_page.html
deleted file mode 100644
index 40d38499babf7c83235490e57b62fe92676ea470..0000000000000000000000000000000000000000
--- a/hypha/public/projects/templates/projects/project_index_page.html
+++ /dev/null
@@ -1 +0,0 @@
-{% extends "utils/listing_index.html" %}
diff --git a/hypha/public/projects/templates/projects/project_page.html b/hypha/public/projects/templates/projects/project_page.html
deleted file mode 100644
index 2b1fc41308dd73f744d43a8dfb2eab88cc5c5a7c..0000000000000000000000000000000000000000
--- a/hypha/public/projects/templates/projects/project_page.html
+++ /dev/null
@@ -1,72 +0,0 @@
-{% extends "base.html" %}
-{% load wagtailcore_tags %}
-
-{% block content %}
-    <div class="wrapper wrapper--small wrapper--inner-space-large">
-        <div class="media-box media-box--reverse">
-            {% include "utils/includes/media_box_icon.html" with page_icon=page.icon %}
-            {% if page.introduction %}
-                <div class="media-box__content">
-                    <h4 class="media-box__teaser media-box__teaser--projectpage-introduction">{{ page.introduction }}</h4>
-                </div>
-            {% endif %}
-        </div>
-
-        <div class="wrapper wrapper--sidebar wrapper--inner-space-small">
-            <div class="wrapper--sidebar--inner">
-                {{ page.body }}
-            </div>
-            <div>
-                {% with contact_details=page.contact_details.all %}
-                    {% if contact_details %}
-                        <h5>Get the word out</h5>
-                        {% for contact in contact_details %}
-                            <a aria-label="Social media link" href="{{ contact.url }}">
-                                <svg class="icon icon--social-share icon--{{contact.service}}-share"><use xlink:href="#{{contact.service}}"></use></svg>
-                            </a>
-                        {% endfor %}
-                    {% endif %}
-                {% endwith %}
-            </div>
-        </div>
-
-
-
-    {# {% include "projects/includes/project_status.html" %} #}
-
-        <div class="wrapper wrapper--inner-space-small">
-            <div>
-                {% include "utils/includes/funding.html" %}
-            </div>
-
-        </div>
-
-
-        {% if page.category_options.all %}
-            <div class="grid grid--two grid--small-gap">
-                {% regroup page.category_options by category as categories %}
-                {% for category, options in categories %}
-                    <div>
-                        <h4>{{ category.name }}</h4>
-                        <ul class="list list--disc">
-                            {% for option in options %}
-                                <li>{{ option.value }}</li>
-                            {% endfor %}
-                        </ul>
-                    </div>
-                {% endfor %}
-            </div>
-        {% endif %}
-
-        {% if page.news_mentions.all %}
-            <h4>We wrote about it</h4>
-            <ul class="list list--disc">
-                {% for news in page.news_mentions.all %}
-                    <li><a href="{% pageurl news.source_page %}">{{ news.source_page }}</a></li>
-                {% endfor %}
-            </ul>
-        {% endif %}
-    </div>
-
-    {% include "includes/relatedcontent.html" with related_pages=page.related_pages.all %}
-{% endblock content %}
diff --git a/hypha/public/projects/templates/projects/widgets/categories_widget.html b/hypha/public/projects/templates/projects/widgets/categories_widget.html
deleted file mode 100644
index 95344239bf4fa9ded866f3cba93e69d3c6ccdf33..0000000000000000000000000000000000000000
--- a/hypha/public/projects/templates/projects/widgets/categories_widget.html
+++ /dev/null
@@ -1 +0,0 @@
-{% spaceless %}<ul class="multiple">{% for widget in widget.subwidgets %}{% include widget.template_name %}{% endfor %}</ul>{% endspaceless %}
diff --git a/hypha/public/projects/templates/projects/widgets/options_option.html b/hypha/public/projects/templates/projects/widgets/options_option.html
deleted file mode 100644
index a1e97f516f91d1ec14a9406f56c35279fe1452d2..0000000000000000000000000000000000000000
--- a/hypha/public/projects/templates/projects/widgets/options_option.html
+++ /dev/null
@@ -1 +0,0 @@
-<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %} style="width:50%;">{{ widget.label }}</label><div class="field-content" style="width:50%;"><div class="input">{% include "django/forms/widgets/input.html" %}</div></div>
diff --git a/hypha/public/projects/templates/projects/widgets/options_widget.html b/hypha/public/projects/templates/projects/widgets/options_widget.html
deleted file mode 100644
index b898b4e8ff96140c545eae2855f57f5050ff5ded..0000000000000000000000000000000000000000
--- a/hypha/public/projects/templates/projects/widgets/options_widget.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<li>
-    <h1>{{ widget.attrs.label_tag }}</h1>
-    <fieldset>
-        {% with id=widget.attrs.id %}
-            <ul{% if id %} id="{{ id }}"{% endif %}class="fields {% if widget.attrs.class %}{{ widget.attrs.class }}{% endif %}">{% for group, options, index in widget.optgroups %}
-                {% for option in options %}
-                    <li {% if id %} id="{{ id }}_{{ index }}"{% endif %}>
-                        <div class="field checkbox_input boolean_field">
-                            {% include option.template_name with widget=option %}{% endfor %}
-                </div>
-                </li>
-            {% endfor %}
-            </ul>{% endwith %}
-    </fieldset>
-</li>
diff --git a/hypha/public/projects/tests.py b/hypha/public/projects/tests.py
deleted file mode 100644
index 10abe618ec899bcb80b642303add4cdf25f5eef2..0000000000000000000000000000000000000000
--- a/hypha/public/projects/tests.py
+++ /dev/null
@@ -1,86 +0,0 @@
-import json
-
-from django.test import TestCase
-
-from hypha.apply.categories.models import Option
-from hypha.apply.categories.tests.factories import CategoryFactory, OptionFactory
-
-from .widgets import CategoriesWidget
-
-
-class TestCategoriesWidget(TestCase):
-    def setUp(self):
-        self.category = CategoryFactory()
-        self.options = OptionFactory.create_batch(3, category=self.category)
-
-    def test_init_has_no_queries(self):
-        with self.assertNumQueries(0):
-            CategoriesWidget()
-
-    def test_can_access_categories_and_options(self):
-        widget = CategoriesWidget()
-        widgets = list(widget.widgets)
-        self.assertEqual(len(widgets), 1)
-        choices = list(widgets[0].choices)
-        self.assertEqual(len(choices), len(self.options))
-        self.assertCountEqual(
-            list(choices), list(Option.objects.values_list("id", "value"))
-        )
-
-    def test_can_get_multiple_categories(self):
-        CategoryFactory()
-        widget = CategoriesWidget()
-        widgets = list(widget.widgets)
-        self.assertEqual(len(widgets), 2)
-
-    def test_can_decompress_data(self):
-        widget = CategoriesWidget()
-        value = json.dumps({self.category.id: [self.options[0].id]})
-        self.assertEqual(widget.decompress(value), [[self.options[0].id]])
-
-    def test_can_decompress_multiple_data(self):
-        new_category = CategoryFactory()
-        widget = CategoriesWidget()
-        value = json.dumps(
-            {
-                self.category.id: [self.options[0].id],
-                new_category.id: [],
-            }
-        )
-        self.assertEqual(widget.decompress(value), [[self.options[0].id], []])
-
-    def test_can_get_data_from_form(self):
-        name = "categories"
-        widget = CategoriesWidget()
-        submitted_data = {
-            name + "_0": [self.options[1].id],
-        }
-
-        value = widget.value_from_datadict(submitted_data, [], name)
-
-        self.assertEqual(value, json.dumps({self.category.id: [self.options[1].id]}))
-
-    def test_can_get_multiple_data_from_form(self):
-        new_category = CategoryFactory()
-        new_options = OptionFactory.create_batch(3, category=new_category)
-
-        name = "categories"
-        widget = CategoriesWidget()
-        answer_1 = [self.options[1].id]
-        answer_2 = [new_options[1].id, new_options[2].id]
-        submitted_data = {
-            name + "_0": answer_1,
-            name + "_1": answer_2,
-        }
-
-        value = widget.value_from_datadict(submitted_data, [], name)
-
-        self.assertEqual(
-            value,
-            json.dumps(
-                {
-                    self.category.id: answer_1,
-                    new_category.id: answer_2,
-                }
-            ),
-        )
diff --git a/hypha/public/projects/views.py b/hypha/public/projects/views.py
deleted file mode 100644
index fd0e0449559b2e00e226cc9f96df7caed44172aa..0000000000000000000000000000000000000000
--- a/hypha/public/projects/views.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# from django.shortcuts import render
-
-# Create your views here.
diff --git a/hypha/public/projects/widgets.py b/hypha/public/projects/widgets.py
deleted file mode 100644
index 9da3c8321c865077a26f4c1c1936622acc57d2c8..0000000000000000000000000000000000000000
--- a/hypha/public/projects/widgets.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import json
-
-from django import forms
-
-from hypha.apply.categories.models import Category
-
-
-class LazyChoices:
-    def __init__(self, queryset, display):
-        self.queryset = queryset
-        self.display = display
-
-    def __iter__(self):
-        for choice in self.queryset.values_list(*self.display):
-            yield choice
-
-
-class LazyWidgets:
-    def __init__(self, widget, model):
-        self.model = model
-        self.widget = widget
-
-    def __iter__(self):
-        for obj in self.model.objects.order_by("id"):
-            yield self.widget(
-                attrs={"id": obj.id, "label_tag": obj.name},
-                choices=LazyChoices(obj.options, ["id", "value"]),
-            )
-
-
-class OptionsWidget(forms.CheckboxSelectMultiple):
-    template_name = "projects/widgets/options_widget.html"
-    option_template_name = "projects/widgets/options_option.html"
-
-    def __init__(self, *args, **kwargs):
-        choices = kwargs["choices"]
-        super().__init__(*args, **kwargs)
-        self.choices = choices
-
-
-class CategoriesWidget(forms.MultiWidget):
-    template_name = "projects/widgets/categories_widget.html"
-
-    def __init__(self, *args, **kwargs):
-        kwargs["widgets"] = []
-        super().__init__(*args, **kwargs)
-        self.widgets = LazyWidgets(OptionsWidget, Category)
-
-    def decompress(self, value):
-        data = json.loads(value)
-        return [data.get(str(widget.attrs["id"]), []) for widget in self.widgets]
-
-    def value_from_datadict(self, data, files, name):
-        data = {
-            widget.attrs["id"]: widget.value_from_datadict(
-                data, files, name + "_%s" % i
-            )
-            for i, widget in enumerate(self.widgets)
-        }
-        return json.dumps(data)
diff --git a/public/sandbox_db.dump b/public/sandbox_db.dump
index 7b2ddbe33d73dd38ad0684f56012a5e20d207222..103c4b8e59422dd8b270bd019b192ea19a951322 100644
Binary files a/public/sandbox_db.dump and b/public/sandbox_db.dump differ