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