We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
group acronym validation crashes in the admin if the group type has not been set.
It should raise a validation error instead if 'type' isn't in the form yet.
See https://github.com/ietf-tools/datatracker/blob/main/ietf/group/admin.py#L47

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `ietf/group/admin.py`
Content:
```
1 # Copyright The IETF Trust 2010-2020, All Rights Reserved
2 # -*- coding: utf-8 -*-
3 
4 import re
5 
6 from functools import update_wrapper
7 
8 import debug # pyflakes:ignore
9 
10 from django import forms
11 
12 from django.contrib import admin
13 from django.contrib.admin.utils import unquote
14 from django.core.management import load_command_class
15 from django.http import Http404
16 from django.shortcuts import render
17 from django.utils.encoding import force_text
18 from django.utils.html import escape
19 from django.utils.translation import ugettext as _
20 
21 from ietf.group.models import (Group, GroupFeatures, GroupHistory, GroupEvent, GroupURL, GroupMilestone,
22     GroupMilestoneHistory, GroupStateTransitions, Role, RoleHistory, ChangeStateGroupEvent,
23     MilestoneGroupEvent, GroupExtResource, )
24 from ietf.name.models import GroupTypeName
25 
26 from ietf.utils.validators import validate_external_resource_value
27 from ietf.utils.response import permission_denied
28 
29 class RoleInline(admin.TabularInline):
30     model = Role
31     raw_id_fields = ["person", "email"]
32 
33 class GroupURLInline(admin.TabularInline):
34     model = GroupURL
35 
36 class GroupForm(forms.ModelForm):
37     class Meta:
38         model = Group
39         fields = '__all__'
40 
41     def clean_acronym(self):
42         ''' Constrain the acronym form. Note that this doesn't look for collisions.
43             See ietf.group.forms.GroupForm.clean_acronym()
44         '''
45         acronym = self.cleaned_data['acronym'].strip().lower()
46         if not self.instance.pk:
47             type = self.cleaned_data['type']
48             if GroupFeatures.objects.get(type=type).has_documents:
49                 if not re.match(r'^[a-z][a-z0-9]+$', acronym):
50                     raise forms.ValidationError("Acronym is invalid, for groups that create documents, the acronym must be at least two characters and only contain lowercase letters and numbers starting with a letter.")
51             else:
52                 if not re.match(r'^[a-z][a-z0-9-]*[a-z0-9]$', acronym):
53                     raise forms.ValidationError("Acronym is invalid, must be at least two characters and only contain lowercase letters and numbers starting with a letter. It may contain hyphens, but that is discouraged.")
54         return acronym
55 
56     def clean_used_roles(self):
57         data = self.cleaned_data['used_roles']
58         if data is None or data == '':
59             raise forms.ValidationError("Must contain a valid json expression. To use the defaults prove an empty list: []")
60         return data
61             
62 
63 class GroupAdmin(admin.ModelAdmin):
64     form = GroupForm
65     list_display = ["acronym", "name", "type", "state", "time", "role_list"]
66     list_display_links = ["acronym", "name"]
67     list_filter = ["type", "state", "time"]
68     search_fields = ["acronym", "name"]
69     ordering = ["name"]
70     raw_id_fields = ["charter", "parent"]
71     inlines = [RoleInline, GroupURLInline]
72     prepopulated_fields = {"acronym": ("name", )}
73 
74     def role_list(self, obj):
75         roles = Role.objects.filter(group=obj).order_by("name", "person__name").select_related('person')
76         res = []
77         for r in roles:
78             res.append('<a href="../../person/person/%s/">%s</a> (<a href="../../group/role/%s/">%s)' % (r.person.pk, escape(r.person.plain_name()), r.pk, r.name.name))
79         return ", ".join(res)
80     role_list.short_description = "Persons" # type: ignore # https://github.com/python/mypy/issues/2087
81     role_list.allow_tags = True         # type: ignore     # https://github.com/python/mypy/issues/2087
82     
83 
84     # SDO reminder
85     def get_urls(self):
86         from ietf.utils.urls import url
87 
88         def wrap(view):
89             def wrapper(*args, **kwargs):
90                 return self.admin_site.admin_view(view)(*args, **kwargs)
91             return update_wrapper(wrapper, view)
92 
93         info = self.model._meta.app_label, self.model._meta.model_name
94 
95         urls = [
96             url(r'^reminder/$', wrap(self.send_reminder), name='%s_%s_reminder' % info),
97             url(r'^(.+)/reminder/$', wrap(self.send_one_reminder), name='%s_%s_one_reminder' % info),
98         ]
99         urls += super(GroupAdmin, self).get_urls()
100         return urls
101 
102     def send_reminder(self, request, sdo=None):
103         opts = self.model._meta
104         app_label = opts.app_label
105 
106         output = None
107         sdo_pk = sdo and sdo.pk or None
108         if request.method == 'POST' and request.POST.get('send', False):
109             command = load_command_class('ietf.liaisons', 'remind_update_sdo_list')
110             output=command.handle(return_output=True, sdo_pk=sdo_pk)
111             output='\n'.join(output)
112 
113         context = {
114             'opts': opts,
115             'has_change_permission': self.has_change_permission(request),
116             'app_label': app_label,
117             'output': output,
118             'sdo': sdo,
119             }
120         return render(request, 'admin/group/group/send_sdo_reminder.html', context )
121 
122 
123     def send_one_reminder(self, request, object_id):
124         model = self.model
125         opts = model._meta
126 
127         try:
128             obj = self.queryset(request).get(pk=unquote(object_id))
129         except model.DoesNotExist:
130             obj = None
131 
132         if not self.has_change_permission(request, obj):
133             permission_denied(request, "You don't have edit permissions for this change.")
134 
135         if obj is None:
136             raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_text(opts.verbose_name), 'key': escape(object_id)})
137 
138         return self.send_reminder(request, sdo=obj)
139     
140 
141 admin.site.register(Group, GroupAdmin)
142 
143 
144 class GroupFeaturesAdminForm(forms.ModelForm):
145     def clean_default_parent(self):
146         # called before form clean() method -- cannot access other fields
147         parent_acro = self.cleaned_data['default_parent'].strip().lower()
148         if len(parent_acro) > 0:
149             if Group.objects.filter(acronym=parent_acro).count() == 0:
150                 raise forms.ValidationError(
151                     'No group exists with acronym "%(acro)s"',
152                     params=dict(acro=parent_acro),
153                 )
154         return parent_acro
155 
156     def clean(self):
157         # cleaning/validation that requires multiple fields
158         parent_acro = self.cleaned_data['default_parent']
159         if len(parent_acro) > 0:
160             parent_type = GroupTypeName.objects.filter(group__acronym=parent_acro).first()
161             if parent_type not in self.cleaned_data['parent_types']:
162                 self.add_error(
163                     'default_parent',
164                     forms.ValidationError(
165                         'Default parent group "%(acro)s" is type "%(gtype)s", which is not an allowed parent type.',
166                         params=dict(acro=parent_acro, gtype=parent_type),
167                     )
168                 )
169 
170 class GroupFeaturesAdmin(admin.ModelAdmin):
171     form = GroupFeaturesAdminForm
172     list_display = [
173         'type',
174         'need_parent',
175         'default_parent',
176         'gf_parent_types',
177         'has_milestones',
178         'has_chartering_process',
179         'has_documents',
180         'has_session_materials',
181         'has_nonsession_materials',
182         'has_meetings',
183         'has_reviews',
184         'has_default_jabber',
185         'acts_like_wg',
186         'create_wiki',
187         'custom_group_roles',
188         'customize_workflow',
189         'is_schedulable',
190         'show_on_agenda',
191         'agenda_filter_type',
192         'req_subm_approval',
193         'agenda_type',
194         'material_types',
195         'admin_roles',
196         'docman_roles',
197         'groupman_roles',
198         'groupman_authroles',
199         'matman_roles',
200         'role_order',
201     ]
202 
203     def gf_parent_types(self, groupfeatures):
204         """Generate list of parent types; needed because many-to-many is not handled automatically"""
205         return ', '.join([gtn.slug for gtn in groupfeatures.parent_types.all()])
206     gf_parent_types.short_description = 'Parent Types'   # type: ignore # https://github.com/python/mypy/issues/2087
207 
208 admin.site.register(GroupFeatures, GroupFeaturesAdmin)
209 
210 class GroupHistoryAdmin(admin.ModelAdmin):
211     list_display = ["time", "acronym", "name", "type"]
212     list_display_links = ["acronym", "name"]
213     list_filter = ["type"]
214     search_fields = ["acronym", "name"]
215     ordering = ["name"]
216     raw_id_fields = ["group", "parent"]
217 
218 admin.site.register(GroupHistory, GroupHistoryAdmin)
219 
220 class GroupURLAdmin(admin.ModelAdmin):
221     list_display = ['id', 'group', 'name', 'url']
222     raw_id_fields = ['group']
223     search_fields = ['name']
224 admin.site.register(GroupURL, GroupURLAdmin)
225 
226 class GroupMilestoneAdmin(admin.ModelAdmin):
227     list_display = ["group", "desc", "due", "resolved", "time"]
228     search_fields = ["group__name", "group__acronym", "desc", "resolved"]
229     raw_id_fields = ["group", "docs"]
230 admin.site.register(GroupMilestone, GroupMilestoneAdmin)
231 admin.site.register(GroupMilestoneHistory, GroupMilestoneAdmin)
232 
233 class GroupStateTransitionsAdmin(admin.ModelAdmin):
234     list_display = ['id', 'group', 'state']
235     raw_id_fields = ['group', 'state']
236 admin.site.register(GroupStateTransitions, GroupStateTransitionsAdmin)
237 
238 class RoleAdmin(admin.ModelAdmin):
239     list_display = ["name", "person", "email", "group"]
240     list_display_links = ["name"]
241     search_fields = ["name__name", "person__name", "email__address"]
242     list_filter = ["name", "group"]
243     ordering = ["id"]
244     raw_id_fields = ["email", "person", "group"]
245 admin.site.register(Role, RoleAdmin)
246 admin.site.register(RoleHistory, RoleAdmin)
247 
248 class GroupEventAdmin(admin.ModelAdmin):
249     list_display = ["id", "group", "time", "type", "by", ]
250     search_fields = ["group__name", "group__acronym"]
251 admin.site.register(GroupEvent, GroupEventAdmin)
252 
253 class ChangeStateGroupEventAdmin(admin.ModelAdmin):
254     list_display = ["id", "group", "state", "time", "type", "by", ]
255     list_filter = ["state", "time", ]
256     search_fields = ["group__name", "group__acronym"]
257 admin.site.register(ChangeStateGroupEvent, ChangeStateGroupEventAdmin)
258 
259 class MilestoneGroupEventAdmin(admin.ModelAdmin):
260     list_display = ['id', 'group', 'time', 'type', 'by', 'desc', 'milestone']
261     list_filter = ['time']
262     raw_id_fields = ['group', 'by', 'milestone']
263 admin.site.register(MilestoneGroupEvent, MilestoneGroupEventAdmin)
264 
265 class GroupExtResourceAdminForm(forms.ModelForm):
266     def clean(self):
267         validate_external_resource_value(self.cleaned_data['name'],self.cleaned_data['value'])
268 
269 class GroupExtResourceAdmin(admin.ModelAdmin):
270     form = GroupExtResourceAdminForm
271     list_display = ['id', 'group', 'name', 'display_name', 'value',]
272     search_fields = ['group__acronym', 'value', 'display_name', 'name__slug',]
273     raw_id_fields = ['group', ]
274 admin.site.register(GroupExtResource, GroupExtResourceAdmin)
275
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/ietf/group/admin.py b/ietf/group/admin.py
--- a/ietf/group/admin.py
+++ b/ietf/group/admin.py
@@ -34,31 +34,50 @@
     model = GroupURL
 
 class GroupForm(forms.ModelForm):
+    # Use CharField with our own validation instead of default SlugField. The real check is in the clean() method.
+    acronym = forms.CharField(min_length=2, max_length=40)
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.fields['type'].required = True  # require this even though the model field can nominally be null
+
     class Meta:
         model = Group
         fields = '__all__'
 
-    def clean_acronym(self):
-        ''' Constrain the acronym form. Note that this doesn't look for collisions.
-            See ietf.group.forms.GroupForm.clean_acronym()
-        '''
-        acronym = self.cleaned_data['acronym'].strip().lower()
-        if not self.instance.pk:
-            type = self.cleaned_data['type']
-            if GroupFeatures.objects.get(type=type).has_documents:
-                if not re.match(r'^[a-z][a-z0-9]+$', acronym):
-                    raise forms.ValidationError("Acronym is invalid, for groups that create documents, the acronym must be at least two characters and only contain lowercase letters and numbers starting with a letter.")
-            else:
-                if not re.match(r'^[a-z][a-z0-9-]*[a-z0-9]$', acronym):
-                    raise forms.ValidationError("Acronym is invalid, must be at least two characters and only contain lowercase letters and numbers starting with a letter. It may contain hyphens, but that is discouraged.")
-        return acronym
-
     def clean_used_roles(self):
         data = self.cleaned_data['used_roles']
         if data is None or data == '':
             raise forms.ValidationError("Must contain a valid json expression. To use the defaults prove an empty list: []")
         return data
-            
+
+    def clean(self):
+        """Clean parts of the form that involve multiple fields"""
+        # Constrain the acronym form. Note that this doesn't look for collisions.
+        # See ietf.group.forms.GroupForm.clean_acronym()
+        if 'acronym' in self.cleaned_data:
+            acronym = self.cleaned_data['acronym'].strip().lower()
+            self.cleaned_data['acronym'] = acronym
+            if 'type' in self.cleaned_data and not self.instance.pk:
+                features = GroupFeatures.objects.get(type=self.cleaned_data['type'])
+                new_and_has_documents = features.has_documents if features else False
+            else:
+                new_and_has_documents = False
+            if new_and_has_documents:
+                valid_re = r'^[a-z][a-z0-9]+$'
+                error_msg = (
+                    'Acronym is invalid. For groups that create documents, the acronym must be at least '
+                    'two characters and only contain lowercase letters and numbers starting with a letter.'
+                )
+            else:
+                valid_re = r'^[a-z][a-z0-9-]*[a-z0-9]$'
+                error_msg = (
+                    'Acronym is invalid. It must be at least two characters and only contain lowercase '
+                    'letters and numbers starting with a letter. It may contain hyphens, but that is discouraged.'
+                )
+            if not re.match(valid_re, acronym):
+                self.add_error('acronym', error_msg)
+
 
 class GroupAdmin(admin.ModelAdmin):
     form = GroupForm

{"golden_diff": "diff --git a/ietf/group/admin.py b/ietf/group/admin.py\n--- a/ietf/group/admin.py\n+++ b/ietf/group/admin.py\n@@ -34,31 +34,50 @@\n     model = GroupURL\n \n class GroupForm(forms.ModelForm):\n+    # Use CharField with our own validation instead of default SlugField. The real check is in the clean() method.\n+    acronym = forms.CharField(min_length=2, max_length=40)\n+\n+    def __init__(self, *args, **kwargs):\n+        super().__init__(*args, **kwargs)\n+        self.fields['type'].required = True  # require this even though the model field can nominally be null\n+\n     class Meta:\n         model = Group\n         fields = '__all__'\n \n-    def clean_acronym(self):\n-        ''' Constrain the acronym form. Note that this doesn't look for collisions.\n-            See ietf.group.forms.GroupForm.clean_acronym()\n-        '''\n-        acronym = self.cleaned_data['acronym'].strip().lower()\n-        if not self.instance.pk:\n-            type = self.cleaned_data['type']\n-            if GroupFeatures.objects.get(type=type).has_documents:\n-                if not re.match(r'^[a-z][a-z0-9]+$', acronym):\n-                    raise forms.ValidationError(\"Acronym is invalid, for groups that create documents, the acronym must be at least two characters and only contain lowercase letters and numbers starting with a letter.\")\n-            else:\n-                if not re.match(r'^[a-z][a-z0-9-]*[a-z0-9]$', acronym):\n-                    raise forms.ValidationError(\"Acronym is invalid, must be at least two characters and only contain lowercase letters and numbers starting with a letter. It may contain hyphens, but that is discouraged.\")\n-        return acronym\n-\n     def clean_used_roles(self):\n         data = self.cleaned_data['used_roles']\n         if data is None or data == '':\n             raise forms.ValidationError(\"Must contain a valid json expression. To use the defaults prove an empty list: []\")\n         return data\n-            \n+\n+    def clean(self):\n+        \"\"\"Clean parts of the form that involve multiple fields\"\"\"\n+        # Constrain the acronym form. Note that this doesn't look for collisions.\n+        # See ietf.group.forms.GroupForm.clean_acronym()\n+        if 'acronym' in self.cleaned_data:\n+            acronym = self.cleaned_data['acronym'].strip().lower()\n+            self.cleaned_data['acronym'] = acronym\n+            if 'type' in self.cleaned_data and not self.instance.pk:\n+                features = GroupFeatures.objects.get(type=self.cleaned_data['type'])\n+                new_and_has_documents = features.has_documents if features else False\n+            else:\n+                new_and_has_documents = False\n+            if new_and_has_documents:\n+                valid_re = r'^[a-z][a-z0-9]+$'\n+                error_msg = (\n+                    'Acronym is invalid. For groups that create documents, the acronym must be at least '\n+                    'two characters and only contain lowercase letters and numbers starting with a letter.'\n+                )\n+            else:\n+                valid_re = r'^[a-z][a-z0-9-]*[a-z0-9]$'\n+                error_msg = (\n+                    'Acronym is invalid. It must be at least two characters and only contain lowercase '\n+                    'letters and numbers starting with a letter. It may contain hyphens, but that is discouraged.'\n+                )\n+            if not re.match(valid_re, acronym):\n+                self.add_error('acronym', error_msg)\n+\n \n class GroupAdmin(admin.ModelAdmin):\n     form = GroupForm\n", "issue": "group acronym validation crashes in the admin if the group type has not been set.\nIt should raise a validation error instead if 'type' isn't in the form yet.\r\nSee https://github.com/ietf-tools/datatracker/blob/main/ietf/group/admin.py#L47\n", "before_files": [{"content": "# Copyright The IETF Trust 2010-2020, All Rights Reserved\n# -*- coding: utf-8 -*-\n\nimport re\n\nfrom functools import update_wrapper\n\nimport debug # pyflakes:ignore\n\nfrom django import forms\n\nfrom django.contrib import admin\nfrom django.contrib.admin.utils import unquote\nfrom django.core.management import load_command_class\nfrom django.http import Http404\nfrom django.shortcuts import render\nfrom django.utils.encoding import force_text\nfrom django.utils.html import escape\nfrom django.utils.translation import ugettext as _\n\nfrom ietf.group.models import (Group, GroupFeatures, GroupHistory, GroupEvent, GroupURL, GroupMilestone,\n    GroupMilestoneHistory, GroupStateTransitions, Role, RoleHistory, ChangeStateGroupEvent,\n    MilestoneGroupEvent, GroupExtResource, )\nfrom ietf.name.models import GroupTypeName\n\nfrom ietf.utils.validators import validate_external_resource_value\nfrom ietf.utils.response import permission_denied\n\nclass RoleInline(admin.TabularInline):\n    model = Role\n    raw_id_fields = [\"person\", \"email\"]\n\nclass GroupURLInline(admin.TabularInline):\n    model = GroupURL\n\nclass GroupForm(forms.ModelForm):\n    class Meta:\n        model = Group\n        fields = '__all__'\n\n    def clean_acronym(self):\n        ''' Constrain the acronym form. Note that this doesn't look for collisions.\n            See ietf.group.forms.GroupForm.clean_acronym()\n        '''\n        acronym = self.cleaned_data['acronym'].strip().lower()\n        if not self.instance.pk:\n            type = self.cleaned_data['type']\n            if GroupFeatures.objects.get(type=type).has_documents:\n                if not re.match(r'^[a-z][a-z0-9]+$', acronym):\n                    raise forms.ValidationError(\"Acronym is invalid, for groups that create documents, the acronym must be at least two characters and only contain lowercase letters and numbers starting with a letter.\")\n            else:\n                if not re.match(r'^[a-z][a-z0-9-]*[a-z0-9]$', acronym):\n                    raise forms.ValidationError(\"Acronym is invalid, must be at least two characters and only contain lowercase letters and numbers starting with a letter. It may contain hyphens, but that is discouraged.\")\n        return acronym\n\n    def clean_used_roles(self):\n        data = self.cleaned_data['used_roles']\n        if data is None or data == '':\n            raise forms.ValidationError(\"Must contain a valid json expression. To use the defaults prove an empty list: []\")\n        return data\n            \n\nclass GroupAdmin(admin.ModelAdmin):\n    form = GroupForm\n    list_display = [\"acronym\", \"name\", \"type\", \"state\", \"time\", \"role_list\"]\n    list_display_links = [\"acronym\", \"name\"]\n    list_filter = [\"type\", \"state\", \"time\"]\n    search_fields = [\"acronym\", \"name\"]\n    ordering = [\"name\"]\n    raw_id_fields = [\"charter\", \"parent\"]\n    inlines = [RoleInline, GroupURLInline]\n    prepopulated_fields = {\"acronym\": (\"name\", )}\n\n    def role_list(self, obj):\n        roles = Role.objects.filter(group=obj).order_by(\"name\", \"person__name\").select_related('person')\n        res = []\n        for r in roles:\n            res.append('<a href=\"../../person/person/%s/\">%s</a> (<a href=\"../../group/role/%s/\">%s)' % (r.person.pk, escape(r.person.plain_name()), r.pk, r.name.name))\n        return \", \".join(res)\n    role_list.short_description = \"Persons\" # type: ignore # https://github.com/python/mypy/issues/2087\n    role_list.allow_tags = True         # type: ignore     # https://github.com/python/mypy/issues/2087\n    \n\n    # SDO reminder\n    def get_urls(self):\n        from ietf.utils.urls import url\n\n        def wrap(view):\n            def wrapper(*args, **kwargs):\n                return self.admin_site.admin_view(view)(*args, **kwargs)\n            return update_wrapper(wrapper, view)\n\n        info = self.model._meta.app_label, self.model._meta.model_name\n\n        urls = [\n            url(r'^reminder/$', wrap(self.send_reminder), name='%s_%s_reminder' % info),\n            url(r'^(.+)/reminder/$', wrap(self.send_one_reminder), name='%s_%s_one_reminder' % info),\n        ]\n        urls += super(GroupAdmin, self).get_urls()\n        return urls\n\n    def send_reminder(self, request, sdo=None):\n        opts = self.model._meta\n        app_label = opts.app_label\n\n        output = None\n        sdo_pk = sdo and sdo.pk or None\n        if request.method == 'POST' and request.POST.get('send', False):\n            command = load_command_class('ietf.liaisons', 'remind_update_sdo_list')\n            output=command.handle(return_output=True, sdo_pk=sdo_pk)\n            output='\\n'.join(output)\n\n        context = {\n            'opts': opts,\n            'has_change_permission': self.has_change_permission(request),\n            'app_label': app_label,\n            'output': output,\n            'sdo': sdo,\n            }\n        return render(request, 'admin/group/group/send_sdo_reminder.html', context )\n\n\n    def send_one_reminder(self, request, object_id):\n        model = self.model\n        opts = model._meta\n\n        try:\n            obj = self.queryset(request).get(pk=unquote(object_id))\n        except model.DoesNotExist:\n            obj = None\n\n        if not self.has_change_permission(request, obj):\n            permission_denied(request, \"You don't have edit permissions for this change.\")\n\n        if obj is None:\n            raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_text(opts.verbose_name), 'key': escape(object_id)})\n\n        return self.send_reminder(request, sdo=obj)\n    \n\nadmin.site.register(Group, GroupAdmin)\n\n\nclass GroupFeaturesAdminForm(forms.ModelForm):\n    def clean_default_parent(self):\n        # called before form clean() method -- cannot access other fields\n        parent_acro = self.cleaned_data['default_parent'].strip().lower()\n        if len(parent_acro) > 0:\n            if Group.objects.filter(acronym=parent_acro).count() == 0:\n                raise forms.ValidationError(\n                    'No group exists with acronym \"%(acro)s\"',\n                    params=dict(acro=parent_acro),\n                )\n        return parent_acro\n\n    def clean(self):\n        # cleaning/validation that requires multiple fields\n        parent_acro = self.cleaned_data['default_parent']\n        if len(parent_acro) > 0:\n            parent_type = GroupTypeName.objects.filter(group__acronym=parent_acro).first()\n            if parent_type not in self.cleaned_data['parent_types']:\n                self.add_error(\n                    'default_parent',\n                    forms.ValidationError(\n                        'Default parent group \"%(acro)s\" is type \"%(gtype)s\", which is not an allowed parent type.',\n                        params=dict(acro=parent_acro, gtype=parent_type),\n                    )\n                )\n\nclass GroupFeaturesAdmin(admin.ModelAdmin):\n    form = GroupFeaturesAdminForm\n    list_display = [\n        'type',\n        'need_parent',\n        'default_parent',\n        'gf_parent_types',\n        'has_milestones',\n        'has_chartering_process',\n        'has_documents',\n        'has_session_materials',\n        'has_nonsession_materials',\n        'has_meetings',\n        'has_reviews',\n        'has_default_jabber',\n        'acts_like_wg',\n        'create_wiki',\n        'custom_group_roles',\n        'customize_workflow',\n        'is_schedulable',\n        'show_on_agenda',\n        'agenda_filter_type',\n        'req_subm_approval',\n        'agenda_type',\n        'material_types',\n        'admin_roles',\n        'docman_roles',\n        'groupman_roles',\n        'groupman_authroles',\n        'matman_roles',\n        'role_order',\n    ]\n\n    def gf_parent_types(self, groupfeatures):\n        \"\"\"Generate list of parent types; needed because many-to-many is not handled automatically\"\"\"\n        return ', '.join([gtn.slug for gtn in groupfeatures.parent_types.all()])\n    gf_parent_types.short_description = 'Parent Types'   # type: ignore # https://github.com/python/mypy/issues/2087\n\nadmin.site.register(GroupFeatures, GroupFeaturesAdmin)\n\nclass GroupHistoryAdmin(admin.ModelAdmin):\n    list_display = [\"time\", \"acronym\", \"name\", \"type\"]\n    list_display_links = [\"acronym\", \"name\"]\n    list_filter = [\"type\"]\n    search_fields = [\"acronym\", \"name\"]\n    ordering = [\"name\"]\n    raw_id_fields = [\"group\", \"parent\"]\n\nadmin.site.register(GroupHistory, GroupHistoryAdmin)\n\nclass GroupURLAdmin(admin.ModelAdmin):\n    list_display = ['id', 'group', 'name', 'url']\n    raw_id_fields = ['group']\n    search_fields = ['name']\nadmin.site.register(GroupURL, GroupURLAdmin)\n\nclass GroupMilestoneAdmin(admin.ModelAdmin):\n    list_display = [\"group\", \"desc\", \"due\", \"resolved\", \"time\"]\n    search_fields = [\"group__name\", \"group__acronym\", \"desc\", \"resolved\"]\n    raw_id_fields = [\"group\", \"docs\"]\nadmin.site.register(GroupMilestone, GroupMilestoneAdmin)\nadmin.site.register(GroupMilestoneHistory, GroupMilestoneAdmin)\n\nclass GroupStateTransitionsAdmin(admin.ModelAdmin):\n    list_display = ['id', 'group', 'state']\n    raw_id_fields = ['group', 'state']\nadmin.site.register(GroupStateTransitions, GroupStateTransitionsAdmin)\n\nclass RoleAdmin(admin.ModelAdmin):\n    list_display = [\"name\", \"person\", \"email\", \"group\"]\n    list_display_links = [\"name\"]\n    search_fields = [\"name__name\", \"person__name\", \"email__address\"]\n    list_filter = [\"name\", \"group\"]\n    ordering = [\"id\"]\n    raw_id_fields = [\"email\", \"person\", \"group\"]\nadmin.site.register(Role, RoleAdmin)\nadmin.site.register(RoleHistory, RoleAdmin)\n\nclass GroupEventAdmin(admin.ModelAdmin):\n    list_display = [\"id\", \"group\", \"time\", \"type\", \"by\", ]\n    search_fields = [\"group__name\", \"group__acronym\"]\nadmin.site.register(GroupEvent, GroupEventAdmin)\n\nclass ChangeStateGroupEventAdmin(admin.ModelAdmin):\n    list_display = [\"id\", \"group\", \"state\", \"time\", \"type\", \"by\", ]\n    list_filter = [\"state\", \"time\", ]\n    search_fields = [\"group__name\", \"group__acronym\"]\nadmin.site.register(ChangeStateGroupEvent, ChangeStateGroupEventAdmin)\n\nclass MilestoneGroupEventAdmin(admin.ModelAdmin):\n    list_display = ['id', 'group', 'time', 'type', 'by', 'desc', 'milestone']\n    list_filter = ['time']\n    raw_id_fields = ['group', 'by', 'milestone']\nadmin.site.register(MilestoneGroupEvent, MilestoneGroupEventAdmin)\n\nclass GroupExtResourceAdminForm(forms.ModelForm):\n    def clean(self):\n        validate_external_resource_value(self.cleaned_data['name'],self.cleaned_data['value'])\n\nclass GroupExtResourceAdmin(admin.ModelAdmin):\n    form = GroupExtResourceAdminForm\n    list_display = ['id', 'group', 'name', 'display_name', 'value',]\n    search_fields = ['group__acronym', 'value', 'display_name', 'name__slug',]\n    raw_id_fields = ['group', ]\nadmin.site.register(GroupExtResource, GroupExtResourceAdmin)\n", "path": "ietf/group/admin.py"}], "after_files": [{"content": "# Copyright The IETF Trust 2010-2020, All Rights Reserved\n# -*- coding: utf-8 -*-\n\nimport re\n\nfrom functools import update_wrapper\n\nimport debug # pyflakes:ignore\n\nfrom django import forms\n\nfrom django.contrib import admin\nfrom django.contrib.admin.utils import unquote\nfrom django.core.management import load_command_class\nfrom django.http import Http404\nfrom django.shortcuts import render\nfrom django.utils.encoding import force_text\nfrom django.utils.html import escape\nfrom django.utils.translation import ugettext as _\n\nfrom ietf.group.models import (Group, GroupFeatures, GroupHistory, GroupEvent, GroupURL, GroupMilestone,\n    GroupMilestoneHistory, GroupStateTransitions, Role, RoleHistory, ChangeStateGroupEvent,\n    MilestoneGroupEvent, GroupExtResource, )\nfrom ietf.name.models import GroupTypeName\n\nfrom ietf.utils.validators import validate_external_resource_value\nfrom ietf.utils.response import permission_denied\n\nclass RoleInline(admin.TabularInline):\n    model = Role\n    raw_id_fields = [\"person\", \"email\"]\n\nclass GroupURLInline(admin.TabularInline):\n    model = GroupURL\n\nclass GroupForm(forms.ModelForm):\n    # Use CharField with our own validation instead of default SlugField. The real check is in the clean() method.\n    acronym = forms.CharField(min_length=2, max_length=40)\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.fields['type'].required = True  # require this even though the model field can nominally be null\n\n    class Meta:\n        model = Group\n        fields = '__all__'\n\n    def clean_used_roles(self):\n        data = self.cleaned_data['used_roles']\n        if data is None or data == '':\n            raise forms.ValidationError(\"Must contain a valid json expression. To use the defaults prove an empty list: []\")\n        return data\n\n    def clean(self):\n        \"\"\"Clean parts of the form that involve multiple fields\"\"\"\n        # Constrain the acronym form. Note that this doesn't look for collisions.\n        # See ietf.group.forms.GroupForm.clean_acronym()\n        if 'acronym' in self.cleaned_data:\n            acronym = self.cleaned_data['acronym'].strip().lower()\n            self.cleaned_data['acronym'] = acronym\n            if 'type' in self.cleaned_data and not self.instance.pk:\n                features = GroupFeatures.objects.get(type=self.cleaned_data['type'])\n                new_and_has_documents = features.has_documents if features else False\n            else:\n                new_and_has_documents = False\n            if new_and_has_documents:\n                valid_re = r'^[a-z][a-z0-9]+$'\n                error_msg = (\n                    'Acronym is invalid. For groups that create documents, the acronym must be at least '\n                    'two characters and only contain lowercase letters and numbers starting with a letter.'\n                )\n            else:\n                valid_re = r'^[a-z][a-z0-9-]*[a-z0-9]$'\n                error_msg = (\n                    'Acronym is invalid. It must be at least two characters and only contain lowercase '\n                    'letters and numbers starting with a letter. It may contain hyphens, but that is discouraged.'\n                )\n            if not re.match(valid_re, acronym):\n                self.add_error('acronym', error_msg)\n\n\nclass GroupAdmin(admin.ModelAdmin):\n    form = GroupForm\n    list_display = [\"acronym\", \"name\", \"type\", \"state\", \"time\", \"role_list\"]\n    list_display_links = [\"acronym\", \"name\"]\n    list_filter = [\"type\", \"state\", \"time\"]\n    search_fields = [\"acronym\", \"name\"]\n    ordering = [\"name\"]\n    raw_id_fields = [\"charter\", \"parent\"]\n    inlines = [RoleInline, GroupURLInline]\n    prepopulated_fields = {\"acronym\": (\"name\", )}\n\n    def role_list(self, obj):\n        roles = Role.objects.filter(group=obj).order_by(\"name\", \"person__name\").select_related('person')\n        res = []\n        for r in roles:\n            res.append('<a href=\"../../person/person/%s/\">%s</a> (<a href=\"../../group/role/%s/\">%s)' % (r.person.pk, escape(r.person.plain_name()), r.pk, r.name.name))\n        return \", \".join(res)\n    role_list.short_description = \"Persons\" # type: ignore # https://github.com/python/mypy/issues/2087\n    role_list.allow_tags = True         # type: ignore     # https://github.com/python/mypy/issues/2087\n    \n\n    # SDO reminder\n    def get_urls(self):\n        from ietf.utils.urls import url\n\n        def wrap(view):\n            def wrapper(*args, **kwargs):\n                return self.admin_site.admin_view(view)(*args, **kwargs)\n            return update_wrapper(wrapper, view)\n\n        info = self.model._meta.app_label, self.model._meta.model_name\n\n        urls = [\n            url(r'^reminder/$', wrap(self.send_reminder), name='%s_%s_reminder' % info),\n            url(r'^(.+)/reminder/$', wrap(self.send_one_reminder), name='%s_%s_one_reminder' % info),\n        ]\n        urls += super(GroupAdmin, self).get_urls()\n        return urls\n\n    def send_reminder(self, request, sdo=None):\n        opts = self.model._meta\n        app_label = opts.app_label\n\n        output = None\n        sdo_pk = sdo and sdo.pk or None\n        if request.method == 'POST' and request.POST.get('send', False):\n            command = load_command_class('ietf.liaisons', 'remind_update_sdo_list')\n            output=command.handle(return_output=True, sdo_pk=sdo_pk)\n            output='\\n'.join(output)\n\n        context = {\n            'opts': opts,\n            'has_change_permission': self.has_change_permission(request),\n            'app_label': app_label,\n            'output': output,\n            'sdo': sdo,\n            }\n        return render(request, 'admin/group/group/send_sdo_reminder.html', context )\n\n\n    def send_one_reminder(self, request, object_id):\n        model = self.model\n        opts = model._meta\n\n        try:\n            obj = self.queryset(request).get(pk=unquote(object_id))\n        except model.DoesNotExist:\n            obj = None\n\n        if not self.has_change_permission(request, obj):\n            permission_denied(request, \"You don't have edit permissions for this change.\")\n\n        if obj is None:\n            raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_text(opts.verbose_name), 'key': escape(object_id)})\n\n        return self.send_reminder(request, sdo=obj)\n    \n\nadmin.site.register(Group, GroupAdmin)\n\n\nclass GroupFeaturesAdminForm(forms.ModelForm):\n    def clean_default_parent(self):\n        # called before form clean() method -- cannot access other fields\n        parent_acro = self.cleaned_data['default_parent'].strip().lower()\n        if len(parent_acro) > 0:\n            if Group.objects.filter(acronym=parent_acro).count() == 0:\n                raise forms.ValidationError(\n                    'No group exists with acronym \"%(acro)s\"',\n                    params=dict(acro=parent_acro),\n                )\n        return parent_acro\n\n    def clean(self):\n        # cleaning/validation that requires multiple fields\n        parent_acro = self.cleaned_data['default_parent']\n        if len(parent_acro) > 0:\n            parent_type = GroupTypeName.objects.filter(group__acronym=parent_acro).first()\n            if parent_type not in self.cleaned_data['parent_types']:\n                self.add_error(\n                    'default_parent',\n                    forms.ValidationError(\n                        'Default parent group \"%(acro)s\" is type \"%(gtype)s\", which is not an allowed parent type.',\n                        params=dict(acro=parent_acro, gtype=parent_type),\n                    )\n                )\n\nclass GroupFeaturesAdmin(admin.ModelAdmin):\n    form = GroupFeaturesAdminForm\n    list_display = [\n        'type',\n        'need_parent',\n        'default_parent',\n        'gf_parent_types',\n        'has_milestones',\n        'has_chartering_process',\n        'has_documents',\n        'has_session_materials',\n        'has_nonsession_materials',\n        'has_meetings',\n        'has_reviews',\n        'has_default_jabber',\n        'acts_like_wg',\n        'create_wiki',\n        'custom_group_roles',\n        'customize_workflow',\n        'is_schedulable',\n        'show_on_agenda',\n        'agenda_filter_type',\n        'req_subm_approval',\n        'agenda_type',\n        'material_types',\n        'admin_roles',\n        'docman_roles',\n        'groupman_roles',\n        'groupman_authroles',\n        'matman_roles',\n        'role_order',\n    ]\n\n    def gf_parent_types(self, groupfeatures):\n        \"\"\"Generate list of parent types; needed because many-to-many is not handled automatically\"\"\"\n        return ', '.join([gtn.slug for gtn in groupfeatures.parent_types.all()])\n    gf_parent_types.short_description = 'Parent Types'   # type: ignore # https://github.com/python/mypy/issues/2087\n\nadmin.site.register(GroupFeatures, GroupFeaturesAdmin)\n\nclass GroupHistoryAdmin(admin.ModelAdmin):\n    list_display = [\"time\", \"acronym\", \"name\", \"type\"]\n    list_display_links = [\"acronym\", \"name\"]\n    list_filter = [\"type\"]\n    search_fields = [\"acronym\", \"name\"]\n    ordering = [\"name\"]\n    raw_id_fields = [\"group\", \"parent\"]\n\nadmin.site.register(GroupHistory, GroupHistoryAdmin)\n\nclass GroupURLAdmin(admin.ModelAdmin):\n    list_display = ['id', 'group', 'name', 'url']\n    raw_id_fields = ['group']\n    search_fields = ['name']\nadmin.site.register(GroupURL, GroupURLAdmin)\n\nclass GroupMilestoneAdmin(admin.ModelAdmin):\n    list_display = [\"group\", \"desc\", \"due\", \"resolved\", \"time\"]\n    search_fields = [\"group__name\", \"group__acronym\", \"desc\", \"resolved\"]\n    raw_id_fields = [\"group\", \"docs\"]\nadmin.site.register(GroupMilestone, GroupMilestoneAdmin)\nadmin.site.register(GroupMilestoneHistory, GroupMilestoneAdmin)\n\nclass GroupStateTransitionsAdmin(admin.ModelAdmin):\n    list_display = ['id', 'group', 'state']\n    raw_id_fields = ['group', 'state']\nadmin.site.register(GroupStateTransitions, GroupStateTransitionsAdmin)\n\nclass RoleAdmin(admin.ModelAdmin):\n    list_display = [\"name\", \"person\", \"email\", \"group\"]\n    list_display_links = [\"name\"]\n    search_fields = [\"name__name\", \"person__name\", \"email__address\"]\n    list_filter = [\"name\", \"group\"]\n    ordering = [\"id\"]\n    raw_id_fields = [\"email\", \"person\", \"group\"]\nadmin.site.register(Role, RoleAdmin)\nadmin.site.register(RoleHistory, RoleAdmin)\n\nclass GroupEventAdmin(admin.ModelAdmin):\n    list_display = [\"id\", \"group\", \"time\", \"type\", \"by\", ]\n    search_fields = [\"group__name\", \"group__acronym\"]\nadmin.site.register(GroupEvent, GroupEventAdmin)\n\nclass ChangeStateGroupEventAdmin(admin.ModelAdmin):\n    list_display = [\"id\", \"group\", \"state\", \"time\", \"type\", \"by\", ]\n    list_filter = [\"state\", \"time\", ]\n    search_fields = [\"group__name\", \"group__acronym\"]\nadmin.site.register(ChangeStateGroupEvent, ChangeStateGroupEventAdmin)\n\nclass MilestoneGroupEventAdmin(admin.ModelAdmin):\n    list_display = ['id', 'group', 'time', 'type', 'by', 'desc', 'milestone']\n    list_filter = ['time']\n    raw_id_fields = ['group', 'by', 'milestone']\nadmin.site.register(MilestoneGroupEvent, MilestoneGroupEventAdmin)\n\nclass GroupExtResourceAdminForm(forms.ModelForm):\n    def clean(self):\n        validate_external_resource_value(self.cleaned_data['name'],self.cleaned_data['value'])\n\nclass GroupExtResourceAdmin(admin.ModelAdmin):\n    form = GroupExtResourceAdminForm\n    list_display = ['id', 'group', 'name', 'display_name', 'value',]\n    search_fields = ['group__acronym', 'value', 'display_name', 'name__slug',]\n    raw_id_fields = ['group', ]\nadmin.site.register(GroupExtResource, GroupExtResourceAdmin)\n", "path": "ietf/group/admin.py"}]}