diff --git a/backend/app/admin.py b/backend/app/admin.py index 488c1ee0..92a2945e 100644 --- a/backend/app/admin.py +++ b/backend/app/admin.py @@ -6,8 +6,9 @@ from . import models models_to_register = [ - models.Pronoun, models.Document, + models.PronounSeries, + models.Gender, ] for model in models_to_register: diff --git a/backend/app/managers.py b/backend/app/managers.py new file mode 100644 index 00000000..003c49c3 --- /dev/null +++ b/backend/app/managers.py @@ -0,0 +1,11 @@ +""" +Custom managers for the gender analysis web app. +""" +from django.db import models + + +class DocumentManager(models.Manager): + def create_document(self, **attributes): + doc = self.create(**attributes) + doc.get_tokenized_text_wc_and_pos() + return doc diff --git a/backend/app/migrations/0003_pronounseries_gender_models.py b/backend/app/migrations/0003_pronounseries_gender_models.py new file mode 100644 index 00000000..23702827 --- /dev/null +++ b/backend/app/migrations/0003_pronounseries_gender_models.py @@ -0,0 +1,37 @@ +# Generated by Django 3.1.5 on 2021-06-25 17:10 + +import app.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0002_document'), + ] + + operations = [ + migrations.CreateModel( + name='PronounSeries', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('identifier', models.CharField(max_length=60)), + ('obj', app.fields.LowercaseCharField(max_length=40)), + ('pos_det', app.fields.LowercaseCharField(max_length=40)), + ('pos_pro', app.fields.LowercaseCharField(max_length=40)), + ('reflex', app.fields.LowercaseCharField(max_length=40)), + ('subj', app.fields.LowercaseCharField(max_length=40)), + ], + ), + migrations.CreateModel( + name='Gender', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.CharField(max_length=60)), + ('pronoun_series', models.ManyToManyField(to='app.PronounSeries')), + ], + ), + migrations.DeleteModel( + name='Pronoun', + ), + ] diff --git a/backend/app/migrations/0005_merge_document_gender.py b/backend/app/migrations/0005_merge_document_gender.py new file mode 100644 index 00000000..9a96311b --- /dev/null +++ b/backend/app/migrations/0005_merge_document_gender.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.5 on 2021-06-25 17:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0004_auto_20210623_1831'), + ('app', '0003_pronounseries_gender_models'), + ] + + operations = [ + ] diff --git a/backend/app/models.py b/backend/app/models.py index b17b3bea..22590197 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -8,40 +8,264 @@ from more_itertools import windowed from django.db import models from .fields import LowercaseCharField +from .managers import DocumentManager -class Pronoun(models.Model): +class PronounSeries(models.Model): """ - A model that allows users to define an individual pronoun and its type - (e.g. subject, object, reflexive, etc). Pronouns are case-insensitive and will be - converted to lowercase. + A class that allows users to define a custom series of pronouns to be used in + analysis functions """ - PRONOUN_TYPES = [ - ('subj', 'Subject'), - ('obj', 'Object'), - ('pos_det', 'Possessive determiner'), - ('pos_pro', 'Possessive pronoun'), - ('reflex', 'Reflexive'), - ] - identifier = LowercaseCharField(max_length=40) - type = models.CharField(max_length=7, choices=PRONOUN_TYPES) + # Things to consider: + # Add a default to reflex? i.e. default = object pronoun + 'self'? + # Also, how to we run doctests here? Or use pytest? (configs don't recognize django package or relative filepath + # in import statement) + identifier = models.CharField(max_length=60) + subj = LowercaseCharField(max_length=40) + obj = LowercaseCharField(max_length=40) + pos_det = LowercaseCharField(max_length=40) + pos_pro = LowercaseCharField(max_length=40) + reflex = LowercaseCharField(max_length=40) + + @property + def all_pronouns(self): + """ + :return: The set of all pronoun identifiers. + """ + pronoun_set = { + self.subj, + self.obj, + self.pos_det, + self.pos_pro, + self.reflex, + } + + return pronoun_set + + def __contains__(self, pronoun): + """ + Checks to see if the given pronoun exists in this group. This check is case-insensitive + >>> pronouns = ['They', 'Them', 'Their', 'Theirs', 'Themself'] + >>> pronoun_group = PronounSeries.objects.create('Andy', *pronouns) + >>> 'they' in pronoun_group + True + >>> 'hers' in pronoun_group + False + :param pronoun: The pronoun to check for in this group + :return: True if the pronoun is in the group, False otherwise + """ + + return pronoun.lower() in self.all_pronouns + + def __iter__(self): + """ + Allows the user to iterate over all of the pronouns in this group. Pronouns + are returned in lowercase and order is not guaranteed. + >>> pronouns = ['she', 'her', 'her', 'hers', 'herself'] + >>> pronoun_group = PronounSeries.objects.create('Fem', *pronouns) + >>> sorted(pronoun_group) + ['her', 'hers', 'herself', 'she'] + """ + + yield from self.all_pronouns + + def __repr__(self): + """ + >>> PronounSeries.objects.create( + ... identifier='Masc', + ... subj='he', + ... obj='him', + ... pos_det='his', + ... pos_pro='his', + ... reflex='himself' + ... ) + + :return: A console-friendly representation of the pronoun series + """ + + return f'<{self.identifier}: {list(sorted(self))}>' + + def __str__(self): + """ + >>> str(PronounSeries.objects.create('Andy', *['Xe', 'Xem', 'Xis', 'Xis', 'Xemself'])) + 'Andy-series' + :return: A string-representation of the pronoun series + """ + + return self.identifier + '-series' + + def __hash__(self): + """ + Makes the `PronounSeries` class hashable + """ + + return self.identifier.__hash__() + + def __eq__(self, other): + """ + Determines whether two `PronounSeries` are equal. Note that they are only equal if + they have the same identifier and the exact same set of pronouns. + + >>> fem_series = PronounSeries.create( + ... identifier='Fem', + ... subj='she', + ... obj='her', + ... pos_det='her', + ... pos_pro='hers', + ... reflex='herself' + ... ) + >>> second_fem_series = PronounSeries.create( + ... identifier='Fem', + ... subj='she', + ... obj='her', + ... pos_pro='hers', + ... reflex='herself' + ... pos_det='HER', + ... ) + >>> fem_series == second_fem_series + True + >>> masc_series = PronounSeries.create( + ... identifier='Masc', + ... subj='he', + ... obj='him', + ... pos_det='his', + ... pos_pro='his', + ... reflex='himself' + ... ) + >>> fem_series == masc_series + False + :param other: The `PronounSeries` object to compare + :return: `True` if the two series are the same, `False` otherwise. + """ + + return ( + self.identifier == other.identifier + and sorted(self) == sorted(other) + ) + + +class Gender(models.Model): + """ + This model defines a gender that analysis functions will use to operate. + """ + + label = models.CharField(max_length=60) + pronoun_series = models.ManyToManyField(PronounSeries) def __repr__(self): - return f'Pronoun({self.identifier, self.type})' + """ + :return: A console-friendly representation of the gender + >>> Gender('Female') + + """ + + return f'<{self.label}>' def __str__(self): - return f'Pronoun: {self.identifier}\nType: {self.get_type_display()}' + """ + :return: A string representation of the gender + >>> str(Gender('Female') + 'Female' + """ + + return self.label + + def __hash__(self): + """ + Allows the Gender object to be hashed + """ + + return self.label.__hash__() def __eq__(self, other): - return self.identifier == other.identifier + """ + Performs a check to see whether two `Gender` objects are equivalent. This is true if and + only if the `Gender` identifiers, pronoun series, and names are identical. + + Note that this comparison works: + >>> fem_pronouns = PronounSeries.objects.create('Fem', *['she', 'her', 'her', 'hers', 'herself']) + + >>> female = Gender.objects.create('Female') + >>> female.pronoun_series.add(1) + + >>> another_female = Gender.objects.create('Female') + >>> another_female.pronoun_series.add(1) + + >>> female == another_female + True + + But this one does not: + >>> they_series = PronounSeries.objects.create('They', *['they', 'them', 'their', 'theirs', 'themselves']) + >>> xe_series = PronounSeries.objects.create('They', *['Xe', 'Xem', 'Xis', 'Xis', 'Xemself']) + >>> androgynous_1 = Gender.objects.create('NB') + >>> androgynous_1.pronoun_series.add(2) + + >>> androgynous_2 = Gender.objects.create('NB') + >>> androgynous_2.pronoun_series.add(3) + + >>> androgynous_1 == androgynous_2 + False + :param other: The other `Gender` object to compare + :return: `True` if the `Gender`s are the same, `False` otherwise + """ + + return ( + self.label == other.label + and list(self.pronoun_series.all()) == list(other.pronoun_series.all()) + ) + + @property + def pronouns(self): + """ + :return: A set containing all pronouns that this `Gender` uses + >>> they_series = PronounSeries.objects.create('They', *['they', 'them', 'their', 'theirs', 'themselves']) + >>> xe_series = PronounSeries('Xe', *['Xe', 'Xer', 'Xis', 'Xis', 'Xerself']) + >>> androgynous = Gender.objects.create('Androgynous') + >>> androgynous.pronoun_series.add(1, 2) + >>> androgynous.pronouns == {'they', 'them', 'theirs', 'xe', 'xer', 'xis'} + True + """ + + all_pronouns = set() + for series in list(self.pronoun_series.all()): + all_pronouns |= series.all_pronouns + + return all_pronouns + + @property + def subj(self): + """ + :return: set of all subject pronouns used to describe the gender + >>> fem_pronouns = PronounSeries('Fem', {'she', 'her', 'hers'}, subj='she', obj='her') + >>> masc_pronouns = PronounSeries('Masc', {'he', 'him', 'his'}, subj='he', obj='him') + >>> bigender = Gender('Bigender', [fem_pronouns, masc_pronouns]) + >>> bigender.subj == {'he', 'she'} + True + """ + + subject_pronouns = set() + for series in list(self.pronoun_series.all()): + subject_pronouns.add(series.subj) + + return subject_pronouns + + @property + def obj(self): + """ + :return: set of all object pronouns used to describe the gender + >>> fem_pronouns = PronounSeries('Fem', {'she', 'her', 'hers'}, subj='she', obj='her') + >>> masc_pronouns = PronounSeries('Masc', {'he', 'him', 'his'}, subj='he', obj='him') + >>> bigender = Gender('Bigender', [fem_pronouns, masc_pronouns]) + >>> bigender.obj == {'him', 'her'} + True + """ -class DocumentManager(models.Manager): - def create_document(self, **attributes): - doc = self.create(**attributes) - doc.get_tokenized_text_wc_and_pos() - return doc + subject_pronouns = set() + for series in list(self.pronoun_series.all()): + subject_pronouns.add(series.obj) + return subject_pronouns class Document(models.Model): diff --git a/backend/app/serializers.py b/backend/app/serializers.py index cca45494..f9cddcb5 100644 --- a/backend/app/serializers.py +++ b/backend/app/serializers.py @@ -6,17 +6,10 @@ # import json from rest_framework import serializers from .models import ( - Pronoun, Document, ) -class PronounSerializer(serializers.ModelSerializer): - class Meta: - model = Pronoun - fields = ['id', 'identifier', 'type'] - - class DocumentSerializer(serializers.ModelSerializer): """ Serializes a Document object diff --git a/backend/app/tests.py b/backend/app/tests.py index f631cf1e..3dde8cbe 100644 --- a/backend/app/tests.py +++ b/backend/app/tests.py @@ -1,44 +1,13 @@ """ Tests for the gender analysis web app. """ - from collections import Counter from django.test import TestCase -from django.core.exceptions import ObjectDoesNotExist from .models import ( - Pronoun, Document, ) -class PronounTestCase(TestCase): - """ - TestCase for the Pronoun model - """ - - def setUp(self): - Pronoun.objects.create(identifier='he', type='subj') - Pronoun.objects.create(identifier='him', type='obj') - Pronoun.objects.create(identifier='HIS', type='pos_det') - Pronoun.objects.create(identifier='his', type='pos_pro') - Pronoun.objects.create(identifier='himself', type='reflex') - - def test_models_save(self): - he = Pronoun.objects.get(identifier='he') - self.assertEqual(str(he), 'Pronoun: he\nType: Subject') - self.assertEqual(type(he.type), str) - - with self.assertRaises(ObjectDoesNotExist): - was_converted_to_lowercase = Pronoun.objects.get(identifier='HIS') - - his = Pronoun.objects.get(identifier='his', type='pos_det') - his_caps_until_saving = Pronoun(identifier='HIS', type='pos_pro') - self.assertNotEqual(his, his_caps_until_saving) - - his_caps_until_saving.save() - self.assertEqual(his, his_caps_until_saving) - - class DocumentTestCase(TestCase): """ Test cases for the Document model @@ -50,15 +19,15 @@ def setUp(self): Document.objects.create_document(title='doc3', text='Do you like ice cream as much as I do?') Document.objects.create(title='doc4', text='This is a ‘very’ “smart” phrase') Document.objects.create_document(title='doc5', text='"This is a quote." There is more. "This is my quote."') - Document.objects.create_document(title='doc6', text="""She took a lighter out of her purse and handed it over to him. - He lit his cigarette and took a deep drag from it, and then began + Document.objects.create_document(title='doc6', text="""She took a lighter out of her purse and handed it over to him. + He lit his cigarette and took a deep drag from it, and then began his speech which ended in a proposal. Her tears drowned the ring.""") - Document.objects.create_document(title='doc7', text="""Hester was convicted of adultery. which made her very sad, - and then Arthur was also sad, and everybody was sad and then + Document.objects.create_document(title='doc7', text="""Hester was convicted of adultery. which made her very sad, + and then Arthur was also sad, and everybody was sad and then Arthur died and it was very sad. Sadness.""") - Document.objects.create_document(title='doc8', text="""Jane was convicted of adultery. she was a beautiful gal, - and everyone thought that she was very beautiful, and everybody - was sad and then she died. Everyone agreed that she was a beautiful + Document.objects.create_document(title='doc8', text="""Jane was convicted of adultery. she was a beautiful gal, + and everyone thought that she was very beautiful, and everybody + was sad and then she died. Everyone agreed that she was a beautiful corpse that deserved peace.""") Document.objects.create_document(title='doc9', text='They refuse to permit us to obtain the refuse permit.') diff --git a/package-lock.json b/package-lock.json index b5101375..5100e716 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8226,9 +8226,9 @@ } }, "webpack": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.38.1.tgz", - "integrity": "sha512-OqRmYD1OJbHZph6RUMD93GcCZy4Z4wC0ele4FXyYF0J6AxO1vOSuIlU1hkS/lDlR9CDYBz64MZRmdbdnFFoT2g==", + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.39.0.tgz", + "integrity": "sha512-25CHmuDj+oOTyteI13sUqNlCnjCnySuhiKWE/cRYPQYeoQ3ijHgyWX27CiyUKLNGq27v8S0mrksyTreT/xo7pg==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.0", diff --git a/package.json b/package.json index a801b638..bf1dc415 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "eslint-webpack-plugin": "^2.5.4", "file-loader": "^6.2.0", "mini-css-extract-plugin": "^1.6.0", - "webpack": "^5.38.1", + "webpack": "^5.39.0", "webpack-cli": "^4.7.2", "webpack-dev-server": "^3.11.2" },