diff --git a/web/config/settings/settings_base.py b/web/config/settings/settings_base.py index dedd3519c..83d371a0c 100644 --- a/web/config/settings/settings_base.py +++ b/web/config/settings/settings_base.py @@ -12,6 +12,7 @@ import os from typing import Any, TypedDict +import json BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) ALLOWED_HOSTS: list[str] = [] @@ -299,3 +300,10 @@ class LoggerConfig(TypedDict, total=False): SENTRY_SEND_DEFAULT_PII = False COVER_IMAGES = False + +# Load ali materials +try: + with open(f"{STATIC_ROOT}/data/ali_materials.json", "r") as file: + ALI_MATERIALS = json.load(file) +except FileNotFoundError as fileNotFoundError: + print("Error opening file.", fileNotFoundError) diff --git a/web/main/migrations/0046_auto_20240516_1322.py b/web/main/migrations/0046_auto_20240516_1322.py new file mode 100644 index 000000000..1dba6ab80 --- /dev/null +++ b/web/main/migrations/0046_auto_20240516_1322.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2024-05-16 13:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0045_add_courtlistener_api_source'), + ] + + operations = [ + migrations.AddField( + model_name='contentnode', + name='ali_licensed', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='historicalcontentnode', + name='ali_licensed', + field=models.BooleanField(default=False), + ) + ] \ No newline at end of file diff --git a/web/main/migrations/0047_auto_20240516_1754.py b/web/main/migrations/0047_auto_20240516_1754.py new file mode 100644 index 000000000..d43ff9bfd --- /dev/null +++ b/web/main/migrations/0047_auto_20240516_1754.py @@ -0,0 +1,67 @@ +# Generated by Django 3.2.25 on 2024-05-16 17:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0046_auto_20240516_1322'), + ] + + operations = [ + migrations.AlterModelOptions( + name='historicalcasebook', + options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical casebook', 'verbose_name_plural': 'historical casebooks'}, + ), + migrations.AlterModelOptions( + name='historicalcontentannotation', + options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical content annotation', 'verbose_name_plural': 'historical content annotations'}, + ), + migrations.AlterModelOptions( + name='historicalcontentnode', + options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical content node', 'verbose_name_plural': 'historical content nodes'}, + ), + migrations.AlterModelOptions( + name='historicallegaldocument', + options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical legal document', 'verbose_name_plural': 'historical legal documents'}, + ), + migrations.AlterModelOptions( + name='historicallink', + options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical link', 'verbose_name_plural': 'historical links'}, + ), + migrations.AlterModelOptions( + name='historicaltextblock', + options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical text block', 'verbose_name_plural': 'historical text blocks'}, + ), + migrations.AlterField( + model_name='historicalcasebook', + name='history_date', + field=models.DateTimeField(db_index=True), + ), + migrations.AlterField( + model_name='historicalcontentannotation', + name='history_date', + field=models.DateTimeField(db_index=True), + ), + migrations.AlterField( + model_name='historicalcontentnode', + name='history_date', + field=models.DateTimeField(db_index=True), + ), + migrations.AlterField( + model_name='historicallegaldocument', + name='history_date', + field=models.DateTimeField(db_index=True), + ), + migrations.AlterField( + model_name='historicallink', + name='history_date', + field=models.DateTimeField(db_index=True), + ), + migrations.AlterField( + model_name='historicaltextblock', + name='history_date', + field=models.DateTimeField(db_index=True), + ), + ] diff --git a/web/main/models.py b/web/main/models.py index 6c70e3869..892a53945 100644 --- a/web/main/models.py +++ b/web/main/models.py @@ -1503,6 +1503,9 @@ class ContentNode( help_text="This content should only be made available on the front end to verified professors", ) + # Indicates whether node includes material licensed by the American Law Institute + ali_licensed = models.BooleanField(default=False) + @classmethod def nodes_for_user_by_casebook( cls, @@ -1653,6 +1656,39 @@ def export_content(self, request): return rich_text_export(self.resource.content, request=request, id_prefix=str(self.id)) return self.resource.content + def get_ali_license_text(self): + """Returns custom license text if title contains all items of the match_words list + + >>> node = getfixture('content_node') + >>> # title including both items in the form of single word strings + >>> node.title = 'Restatement (Third) of Property (Servitudes) Notes and Questions' + >>> assert node.get_ali_license_text() == 'Restatement of the Law, Property, copyright @ 1977-2023 by the American Law Institute. Reproduced with permission, not as part of a Creative Commons license.' + >>> # title including both items in the form of one single word string and one multi-word string + >>> node.title = 'Excerpts from Restatement (First) and (Second) of Conflict of Laws' + >>> assert node.get_ali_license_text() == 'Restatement of the Law, Conflict of Laws, copyright @ 1971-2023 by the American Law Institute. Reproduced with permission, not as part of a Creative Commons license.' + >>> # title including both items in the form of two multi-word strings in lower case + >>> node.title = 'section from model penal code sexual assault and related offenses publication' + >>> assert node.get_ali_license_text() == 'Model Penal Code, Sexual Assault and Related Offenses, copyright @ 2021-2022 by the American Law Institute. Reproduced with permission, not as part of a Creative Commons license.' + >>> # title that only includes one of the words + >>> node.title = 'section from the principles of law publications' + >>> assert node.get_ali_license_text() == '' + >>> # title that doesn't include any of the words + >>> node.title = 'Text that does not include any of the match words' + >>> assert node.get_ali_license_text() == '' + """ + + title = self.title.lower() + license_txt = "" + + for item in settings.ALI_MATERIALS: + if all(word in title for word in item["match_words"]): + license_txt = ( + f"{item['title']}, copyright @ {item['years']} by the American Law Institute. " + f"Reproduced with permission, not as part of a Creative Commons license." + ) + break + return license_txt + @property def is_temporary(self): return self.resource_type == "Temp" @@ -1765,6 +1801,7 @@ def save(self, *args, **kwargs): """ cleanse_html_field(self, "headnote", True) self.headnote_doc_class = self.identify_headnote_type() + self.ali_licensed = bool(self.get_ali_license_text()) super().save(*args, **kwargs) def delete(self, *args, **kwargs): diff --git a/web/main/templates/includes/casebook_copyright_notice.html b/web/main/templates/includes/casebook_copyright_notice.html index 496d1d335..d66c998f0 100644 --- a/web/main/templates/includes/casebook_copyright_notice.html +++ b/web/main/templates/includes/casebook_copyright_notice.html @@ -3,8 +3,11 @@ for sharing and re-use with the exception of certain excerpts. - {# TODO make this dynamic in response to this casebook's content #} + {% if section.ali_licensed %} + {{ section.get_ali_license_text }} + {% else %} Any excerpts from the Restatements of the Law, Principles of the Law, and the Model Penal Code are copyright by The American Law Institute. Excerpts are reproduced with permission, not as part of a Creative Commons license. + {% endif %}

diff --git a/web/static/data/ali_materials.json b/web/static/data/ali_materials.json new file mode 100644 index 000000000..c7ddb14a6 --- /dev/null +++ b/web/static/data/ali_materials.json @@ -0,0 +1,210 @@ +[ + { + "title": "Restatement of the Law, Agency", + "years": "2006-2023", + "match_words": [ + "restatement", + "agency" + ] + }, + { + "title": "Restatement of the Law, The Law of American Indians", + "years": "2022-2023", + "match_words": [ + "restatement", + "the law of american indians" + ] + }, + { + "title": "Restatement of the Law, Charitable Nonprofit Organizations", + "years": "2021-2023", + "match_words": [ + "restatement", + "charitable nonprofit organizations" + ] + }, + { + "title": "Restatement of the Law, Conflict of Laws", + "years": "1971-2023", + "match_words": [ + "restatement", + "conflict of laws" + ] + }, + { + "title": "Restatement of the Law, Contracts", + "years": "1981-2023", + "match_words": [ + "restatement", + "contracts" + ] + }, + { + "title": "Restatement of the Law, Employment Law", + "years": "2015-2023", + "match_words": [ + "restatement", + "employment law" + ] + }, + { + "title": "Restatement of the Law, Foreign Relations Law of the United States", + "years": "1987-2023", + "match_words": [ + "restatement", + "foreign relations law of the united states" + ] + }, + { + "title": "Restatement of the Law, Judgments", + "years": "1982-2023", + "match_words": [ + "restatement", + "judgments" + ] + }, + { + "title": "Restatement of the Law, The Law Governing Lawyers", + "years": "2000-2023", + "match_words": [ + "restatement", + "the law governing lawyers" + ] + }, + { + "title": "Restatement of the Law, Liability Insurance", + "years": "2019-2023", + "match_words": [ + "restatement", + "liability insurance" + ] + }, + { + "title": "Restatement of the Law, Property", + "years": "1977-2023", + "match_words": [ + "restatement", + "property" + ] + }, + { + "title": "Restatement of the Law, Restitution and Unjust Enrichment", + "years": "2011-2023", + "match_words": [ + "restatement", + "restitution and unjust enrichment" + ] + }, + { + "title": "Restatement of the Law, Suretyship and Guaranty", + "years": "1996-2023", + "match_words": [ + "restatement", + "suretyship and guaranty" + ] + }, + { + "title": "Restatement of the Law, Torts", + "years": "1965-2023", + "match_words": [ + "restatement", + "torts" + ] + }, + { + "title": "Restatement of the Law, Trusts", + "years": "2003-2023", + "match_words": [ + "restatement", + "trusts" + ] + }, + { + "title": "Restatement of the Law, Unfair Competition", + "years": "1995-2023", + "match_words": [ + "restatement", + "unfair competition" + ] + }, + { + "title": "Restatement of the Law, The U.S. Law of International Commercial and Investor-State Arbitration", + "years": "2023", + "match_words": [ + "restatement", + "the u.s. law of international commercial and investor-state arbitrationn" + ] + }, + { + "title": "Principles of the Law, Aggregate Litigation", + "years": "2010-2023", + "match_words": [ + "principle", + "aggregate litigation" + ] + }, + { + "title": "Principles of the Law, Corporate Governance", + "years": "1994-2023", + "match_words": [ + "principle", + "corporate governance" + ] + }, + { + "title": "Principles of the Law, Data Privacy", + "years": "2020", + "match_words": [ + "principle", + "data privacy" + ] + }, + { + "title": "Principles of the Law, Election Administration: Non-Precinct Voting and Resolution of Ballot-Counting Disputes", + "years": "2019-2023", + "match_words": [ + "principle", + "election administration: non-precinct voting and resolution of ballot-counting disputes" + ] + }, + { + "title": "Principles of the Law, Family Dissolution", + "years": "2002-2023", + "match_words": [ + "principle", + "family dissolution" + ] + }, + { + "title": "Principles of the Law, Intellectual Property: Principles Governing Jurisdiction, Choice of Law, and Judgments in Transnational Disputes", + "years": "2008", + "match_words": [ + "principle", + "intellectual property: principles governing jurisdiction, choice of law, and judgments in transnational disputes" + ] + }, + { + "title": "Principles of the Law, Software Contracts", + "years": "2010", + "match_words": [ + "principle", + "software contracts" + ] + }, + { + "title": "Model Penal Code, Sentencing", + "years": "2023", + "match_words": [ + "model penal code", + "sentencing" + ] + }, + { + "title": "Model Penal Code, Sexual Assault and Related Offenses", + "years": "2021-2022", + "match_words": [ + "model penal code", + "sexual assault and related offenses" + ] + } +] \ No newline at end of file diff --git a/web/tasks.py b/web/tasks.py index b26b27bc6..a18c38eb2 100644 --- a/web/tasks.py +++ b/web/tasks.py @@ -465,3 +465,18 @@ def export_node( print( f"Generated export file ({filesizeformat(len(file_contents))}) in {round(after-before,2)} seconds." ) + + +@task +@setup_django +def populate_ali_licensed(ctx): + """ + Retroactively update ContentNode's 'ali_licensed' field + """ + from main.models import ContentNode + + for node in tqdm(ContentNode.objects.iterator()): + is_licensed = bool(node.get_ali_license_text()) + if node.ali_licensed is not is_licensed: + node.ali_licensed = is_licensed + node.save(update_fields=["ali_licensed"])