Coverage for src/ptf_tools/forms.py: 38%

365 statements  

« prev     ^ index     » next       coverage.py v7.9.0, created at 2025-11-26 13:32 +0000

1import glob 

2import os 

3from operator import itemgetter 

4 

5from allauth.account.forms import SignupForm as BaseSignupForm 

6from ckeditor_uploader.fields import RichTextUploadingFormField 

7from crispy_forms.helper import FormHelper 

8from django import forms 

9from django.conf import settings 

10from django.contrib.auth import get_user_model 

11from invitations.forms import CleanEmailMixin 

12from mersenne_cms.models import News, Page 

13from ptf.model_helpers import get_collection_id, is_site_en_only, is_site_fr_only 

14from ptf.models import ( 

15 BibItemId, 

16 Collection, 

17 ExtId, 

18 ExtLink, 

19 GraphicalAbstract, 

20 RelatedArticles, 

21 ResourceId, 

22 Title, 

23) 

24 

25from .models import Invitation, InvitationExtraData 

26 

27TYPE_CHOICES = ( 

28 ("doi", "doi"), 

29 ("mr-item-id", "mr"), 

30 ("zbl-item-id", "zbl"), 

31 ("numdam-id", "numdam"), 

32 ("pmid", "pubmed"), 

33) 

34 

35RESOURCE_ID_CHOICES = ( 

36 ("issn", "p-issn"), 

37 ("e-issn", "e-issn"), 

38) 

39 

40REL_CHOICES = ( 

41 ("small_icon", "small_icon"), 

42 ("icon", "icon"), 

43 ("test_website", "test_website"), 

44 ("website", "website"), 

45) 

46 

47IMPORT_CHOICES = ( 

48 ("1", "Préserver des métadonnées existantes dans ptf-tools (equal-contrib, coi_statement)"), 

49 ("2", "Remplacer tout par le fichier XML"), 

50) 

51 

52 

53class PtfFormHelper(FormHelper): 

54 def __init__(self, *args, **kwargs): 

55 super().__init__(*args, **kwargs) 

56 self.label_class = "col-xs-4 col-sm-2" 

57 self.field_class = "col-xs-8 col-sm-6" 

58 self.form_tag = False 

59 

60 

61class PtfModalFormHelper(FormHelper): 

62 def __init__(self, *args, **kwargs): 

63 super().__init__(*args, **kwargs) 

64 self.label_class = "col-xs-3" 

65 self.field_class = "col-xs-8" 

66 self.form_tag = False 

67 

68 

69class PtfLargeModalFormHelper(FormHelper): 

70 def __init__(self, *args, **kwargs): 

71 super().__init__(*args, **kwargs) 

72 self.label_class = "col-xs-6" 

73 self.field_class = "col-xs-6" 

74 self.form_tag = False 

75 

76 

77class FormSetHelper(FormHelper): 

78 def __init__(self, *args, **kwargs): 

79 super().__init__(*args, **kwargs) 

80 self.form_tag = False 

81 self.template = "bootstrap3/whole_uni_formset.html" 

82 

83 

84class BibItemIdForm(forms.ModelForm): 

85 class Meta: 

86 model = BibItemId 

87 fields = ["bibitem", "id_type", "id_value"] 

88 

89 def __init__(self, *args, **kwargs): 

90 super().__init__(*args, **kwargs) 

91 self.fields["id_type"].widget = forms.Select(choices=TYPE_CHOICES) 

92 self.fields["bibitem"].widget = forms.HiddenInput() 

93 

94 

95class ExtIdForm(forms.ModelForm): 

96 class Meta: 

97 model = ExtId 

98 fields = ["resource", "id_type", "id_value"] 

99 

100 def __init__(self, *args, **kwargs): 

101 super().__init__(*args, **kwargs) 

102 self.fields["id_type"].widget = forms.Select(choices=TYPE_CHOICES) 

103 self.fields["resource"].widget = forms.HiddenInput() 

104 

105 

106class ExtLinkForm(forms.ModelForm): 

107 class Meta: 

108 model = ExtLink 

109 fields = ["rel", "location"] 

110 widgets = { 

111 "rel": forms.Select(choices=REL_CHOICES), 

112 } 

113 

114 

115class ResourceIdForm(forms.ModelForm): 

116 class Meta: 

117 model = ResourceId 

118 fields = ["id_type", "id_value"] 

119 widgets = { 

120 "id_type": forms.Select(choices=RESOURCE_ID_CHOICES), 

121 } 

122 

123 

124class CollectionForm(forms.ModelForm): 

125 class Meta: 

126 model = Collection 

127 fields = [ 

128 "pid", 

129 "provider", 

130 "coltype", 

131 "title_tex", 

132 "abbrev", 

133 "doi", 

134 "wall", 

135 "alive", 

136 "sites", 

137 ] 

138 widgets = { 

139 "title_tex": forms.TextInput(), 

140 } 

141 

142 def __init__(self, *args, **kwargs): 

143 # Add extra fields before the base class __init__ 

144 self.base_fields["description_en"] = RichTextUploadingFormField( 

145 required=False, label="Description (EN)" 

146 ) 

147 self.base_fields["description_fr"] = RichTextUploadingFormField( 

148 required=False, label="Description (FR)" 

149 ) 

150 

151 super().__init__(*args, **kwargs) 

152 

153 # self.instance is now set, specify initial values 

154 qs = self.instance.abstract_set.filter(tag="description") 

155 for abstract in qs: 

156 if abstract.lang == "fr": 

157 self.initial["description_fr"] = abstract.value_html 

158 elif abstract.lang == "en": 

159 self.initial["description_en"] = abstract.value_html 

160 

161 

162class ContainerForm(forms.Form): 

163 pid = forms.CharField(required=True, initial="") 

164 title = forms.CharField(required=False, initial="") 

165 trans_title = forms.CharField(required=False, initial="") 

166 publisher = forms.CharField(required=True, initial="") 

167 year = forms.CharField(required=False, initial="") 

168 volume = forms.CharField(required=False, initial="") 

169 number = forms.CharField(required=False, initial="") 

170 icon = forms.FileField(required=False) 

171 

172 def __init__(self, container, *args, **kwargs): 

173 super().__init__(*args, **kwargs) 

174 self.container = container 

175 

176 if "data" in kwargs: 

177 # form_invalid: preserve input values 

178 self.fields["pid"].initial = kwargs["data"]["pid"] 

179 self.fields["publisher"].initial = kwargs["data"]["publisher"] 

180 self.fields["year"].initial = kwargs["data"]["year"] 

181 self.fields["volume"].initial = kwargs["data"]["volume"] 

182 self.fields["number"].initial = kwargs["data"]["number"] 

183 self.fields["title"].initial = kwargs["data"]["title"] 

184 self.fields["trans_title"].initial = kwargs["data"]["trans_title"] 

185 elif container: 

186 self.fields["pid"].initial = container.pid 

187 self.fields["title"].initial = container.title_tex 

188 if self.container.trans_lang and self.container.trans_lang != "und": 

189 try: 

190 self.fields["trans_title"].initial = Title.objects.get( 

191 resource=self.container, lang=self.container.trans_lang 

192 ).title_html 

193 except Title.DoesNotExist: 

194 self.fields["trans_title"].initial = container.trans_title_tex 

195 else: 

196 self.fields["trans_title"].initial = container.trans_title_tex 

197 

198 if container.my_publisher: 

199 self.fields["publisher"].initial = container.my_publisher.pub_name 

200 self.fields["year"].initial = container.year 

201 self.fields["volume"].initial = container.volume 

202 self.fields["number"].initial = container.number 

203 

204 for extlink in container.extlink_set.all(): 

205 if extlink.rel == "icon": 

206 self.fields["icon"].initial = os.path.basename(extlink.location) 

207 

208 def clean(self): 

209 cleaned_data = super().clean() 

210 return cleaned_data 

211 

212 

213class ArticleForm(forms.Form): 

214 pid = forms.CharField(required=True, initial="") 

215 title = forms.CharField(required=False, initial="") 

216 fpage = forms.CharField(required=False, initial="") 

217 lpage = forms.CharField(required=False, initial="") 

218 page_count = forms.CharField(required=False, initial="") 

219 page_range = forms.CharField(required=False, initial="") 

220 icon = forms.FileField(required=False) 

221 pdf = forms.FileField(required=False) 

222 coi_statement = forms.CharField(required=False, initial="") 

223 show_body = forms.BooleanField(required=False, initial=True) 

224 do_not_publish = forms.BooleanField(required=False, initial=True) 

225 

226 def __init__(self, article, *args, **kwargs): 

227 super().__init__(*args, **kwargs) 

228 self.article = article 

229 

230 if "data" in kwargs: 

231 data = kwargs["data"] 

232 # form_invalid: preserve input values 

233 self.fields["pid"].initial = data["pid"] 

234 if "title" in data: 

235 self.fields["title"].initial = data["title"] 

236 if "fpage" in data: 

237 self.fields["fpage"].initial = data["fpage"] 

238 if "lpage" in data: 

239 self.fields["lpage"].initial = data["lpage"] 

240 if "page_range" in data: 

241 self.fields["page_range"].initial = data["page_range"] 

242 if "page_count" in data: 

243 self.fields["page_count"].initial = data["page_count"] 

244 if "coi_statement" in data: 

245 self.fields["coi_statement"].initial = data["coi_statement"] 

246 if "show_body" in data: 

247 self.fields["show_body"].initial = data["show_body"] 

248 if "do_not_publish" in data: 

249 self.fields["do_not_publish"].initial = data["do_not_publish"] 

250 elif article: 

251 # self.fields['pid'].initial = article.pid 

252 self.fields["title"].initial = article.title_tex 

253 self.fields["fpage"].initial = article.fpage 

254 self.fields["lpage"].initial = article.lpage 

255 self.fields["page_range"].initial = article.page_range 

256 self.fields["coi_statement"].initial = ( 

257 article.coi_statement if article.coi_statement else "" 

258 ) 

259 self.fields["show_body"].initial = article.show_body 

260 self.fields["do_not_publish"].initial = article.do_not_publish 

261 

262 for count in article.resourcecount_set.all(): 

263 if count.name == "page-count": 

264 self.fields["page_count"].initial = count.value 

265 

266 for extlink in article.extlink_set.all(): 

267 if extlink.rel == "icon": 

268 self.fields["icon"].initial = os.path.basename(extlink.location) 

269 

270 qs = article.datastream_set.filter(rel="full-text", mimetype="application/pdf") 

271 if qs.exists(): 

272 datastream = qs.first() 

273 self.fields["pdf"].initial = datastream.location 

274 

275 def clean(self): 

276 cleaned_data = super().clean() 

277 return cleaned_data 

278 

279 

280def cast_volume(element): 

281 # Permet le classement des volumes dans le cas où : 

282 # - un numero de volume est de la forme "11-12" (cf crchim) 

283 # - un volume est de la forme "S5" (cf smai) 

284 if not element: 

285 return "", "" 

286 try: 

287 casted = int(element.split("-")[0]) 

288 extra = "" 

289 except ValueError as _: 

290 casted = int(element.split("-")[0][1:]) 

291 extra = element 

292 return extra, casted 

293 

294 

295def unpack_pid(filename): 

296 # retourne un tableau pour chaque filename de la forme : 

297 # [filename, collection, year, vseries, volume_extra, volume, issue_extra, issue] 

298 # Permet un tri efficace par la suite 

299 collection, year, vseries, volume, issue = filename.split("/")[-1].split(".")[0].split("_") 

300 extra_volume, casted_volume = cast_volume(volume) 

301 extra_issue, casted_issue = cast_volume(issue) 

302 return ( 

303 filename, 

304 collection, 

305 year, 

306 vseries, 

307 extra_volume, 

308 casted_volume, 

309 extra_issue, 

310 casted_issue, 

311 ) 

312 

313 

314def get_volume_choices(colid, to_appear=False): 

315 if settings.IMPORT_CEDRICS_DIRECTLY: 

316 collection_folder = os.path.join(settings.CEDRAM_TEX_FOLDER, colid) 

317 

318 if to_appear: 

319 issue_folders = [ 

320 volume for volume in os.listdir(collection_folder) if f"{colid}_0" in volume 

321 ] 

322 

323 else: 

324 issue_folders = [ 

325 d 

326 for d in os.listdir(collection_folder) 

327 if os.path.isdir(os.path.join(collection_folder, d)) 

328 ] 

329 issue_folders = sorted(issue_folders, reverse=True) 

330 

331 files = [ 

332 (os.path.join(collection_folder, d, d + "-cdrxml.xml"), d) 

333 for d in issue_folders 

334 if os.path.isfile(os.path.join(collection_folder, d, d + "-cdrxml.xml")) 

335 ] 

336 else: 

337 if to_appear: 

338 volumes_path = os.path.join( 

339 settings.CEDRAM_XML_FOLDER, colid, "metadata", f"{colid}_0*.xml" 

340 ) 

341 else: 

342 volumes_path = os.path.join(settings.CEDRAM_XML_FOLDER, colid, "metadata", "*.xml") 

343 

344 files = [unpack_pid(filename) for filename in glob.glob(volumes_path)] 

345 sort = sorted(files, key=itemgetter(1, 2, 3, 4, 5, 6, 7), reverse=True) 

346 files = [(item[0], item[0].split("/")[-1]) for item in sort] 

347 return files 

348 

349 

350def get_article_choices(colid, issue_name): 

351 issue_folder = os.path.join(settings.CEDRAM_TEX_FOLDER, colid, issue_name) 

352 article_choices = [ 

353 (d, os.path.basename(d)) 

354 for d in os.listdir(issue_folder) 

355 if ( 

356 os.path.isdir(os.path.join(issue_folder, d)) 

357 and os.path.isfile(os.path.join(issue_folder, d, d + "-cdrxml.xml")) 

358 ) 

359 ] 

360 article_choices = sorted(article_choices, reverse=True) 

361 

362 return article_choices 

363 

364 

365class ImportArticleForm(forms.Form): 

366 issue = forms.ChoiceField( 

367 label="Numéro", 

368 ) 

369 article = forms.ChoiceField( 

370 label="Article", 

371 ) 

372 

373 def __init__(self, *args, **kwargs): 

374 # we need to pop this extra colid kwarg if not, the call to super.__init__ won't work 

375 colid = kwargs.pop("colid") 

376 super().__init__(*args, **kwargs) 

377 volumes = get_volume_choices(colid) 

378 self.fields["issue"].choices = volumes 

379 articles = [] 

380 if volumes: 

381 articles = get_article_choices(colid, volumes[0][1]) 

382 self.fields["article"].choices = articles 

383 

384 

385class ImportContainerForm(forms.Form): 

386 filename = forms.ChoiceField( 

387 label="Numéro", 

388 ) 

389 remove_email = forms.BooleanField( 

390 label="Supprimer les mails des contribs issus de CEDRAM ?", 

391 initial=True, 

392 required=False, 

393 ) 

394 remove_date_prod = forms.BooleanField( 

395 label="Supprimer les dates de mise en prod issues de CEDRAM ?", 

396 initial=True, 

397 required=False, 

398 ) 

399 

400 def __init__(self, *args, **kwargs): 

401 # we need to pop this extra colid kwarg if not, the call to super.__init__ won't work 

402 colid = kwargs.pop("colid") 

403 to_appear = kwargs.pop("to_appear") 

404 super().__init__(*args, **kwargs) 

405 self.fields["filename"].choices = get_volume_choices(colid, to_appear) 

406 

407 

408class DiffContainerForm(forms.Form): 

409 import_choice = forms.ChoiceField( 

410 choices=IMPORT_CHOICES, label="Que faire des différences ?", widget=forms.RadioSelect() 

411 ) 

412 

413 def __init__(self, *args, **kwargs): 

414 # we need to pop this extra full_path kwarg if not, the call to super.__init__ won't work 

415 kwargs.pop("colid") 

416 # filename = kwargs.pop('filename') 

417 # to_appear = kwargs.pop('to_appear') 

418 super().__init__(*args, **kwargs) 

419 

420 self.fields["import_choice"].initial = IMPORT_CHOICES[0][0] 

421 

422 

423class ImportEditflowArticleForm(forms.Form): 

424 @staticmethod 

425 def validate_xml_file(value): 

426 if not value.name.lower().endswith(".xml"): 

427 raise forms.ValidationError("Only .xml files are allowed.") 

428 

429 editflow_xml_file = forms.FileField( 

430 label="Import an article from an XML file provided by Editflow.", 

431 required=True, 

432 validators=[validate_xml_file.__func__], 

433 widget=forms.ClearableFileInput(attrs={"accept": ".xml"}), 

434 help_text="Only .xml files are accepted.", 

435 ) 

436 

437 

438class RegisterPubmedForm(forms.Form): 

439 CHOICES = [ 

440 ("off", "Yes"), 

441 ("on", "No, update the article in PubMed"), 

442 ] 

443 update_article = forms.ChoiceField( 

444 label="Are you registering the article for the first time ? Note: If you are updating the article, consider that only AuthorList (Author, Affiliation, Identifier), InvestigatorList (Investigator, Affiliation, Identifier), Pagination, ELocationID, OtherAbstract, PII and DOI fields can be updated. All other edits must be made using the PubMed Data Management system: https://www.ncbi.nlm.nih.gov/pubmed/management/", 

445 widget=forms.RadioSelect, 

446 choices=CHOICES, 

447 required=False, 

448 initial="on", 

449 ) 

450 

451 

452class CreateFrontpageForm(forms.Form): 

453 create_frontpage = forms.BooleanField( 

454 label="Update des frontpages des articles avec date de mise en ligne ?", 

455 initial=False, 

456 required=False, 

457 ) 

458 

459 

460class RelatedForm(forms.ModelForm): 

461 doi_list = forms.CharField( 

462 required=False, 

463 widget=forms.Textarea(attrs={"rows": "10", "placeholder": "doi_1\ndoi_2\ndoi_3\n"}), 

464 ) 

465 

466 exclusion_list = forms.CharField( 

467 required=False, 

468 widget=forms.Textarea(attrs={"rows": "10"}), 

469 ) 

470 

471 class Meta: 

472 model = RelatedArticles 

473 fields = ["doi_list", "exclusion_list", "automatic_list"] 

474 

475 

476class GraphicalAbstractForm(forms.ModelForm): 

477 """Form for the Graphical Abstract model""" 

478 

479 class Meta: 

480 model = GraphicalAbstract 

481 fields = ("graphical_abstract", "illustration") 

482 

483 

484class PageForm(forms.ModelForm): 

485 class Meta: 

486 model = Page 

487 fields = [ 

488 "menu_title_en", 

489 "menu_title_fr", 

490 "parent_page", 

491 "content_en", 

492 "content_fr", 

493 "state", 

494 "slug_en", 

495 "slug_fr", 

496 "menu_order", 

497 "position", 

498 "mersenne_id", 

499 "site_id", 

500 ] 

501 

502 def __init__(self, *args, **kwargs): 

503 site_id = kwargs.pop("site_id") 

504 user = kwargs.pop("user") 

505 super().__init__(*args, **kwargs) 

506 

507 self.fields["site_id"].initial = site_id 

508 

509 if not user.is_staff: 509 ↛ 516line 509 didn't jump to line 516 because the condition on line 509 was always true

510 for field_name in ["mersenne_id", "site_id"]: 

511 field = self.fields[field_name] 

512 # Hide the field is not enough, otherwise BaseForm._clean_fields will not get the value 

513 field.disabled = True 

514 field.widget = field.hidden_widget() 

515 

516 colid = get_collection_id(int(site_id)) 

517 

518 # By default, CKEditor stores files in 1 folder 

519 # We want to store the files in a @colid folder 

520 for field_name in ["content_en", "content_fr"]: 

521 field = self.fields[field_name] 

522 widget = field.widget 

523 widget.config["filebrowserUploadUrl"] = "/ckeditor/upload/" + colid 

524 widget.config["filebrowserBrowseUrl"] = "/ckeditor/browse/" + colid 

525 

526 pages = Page.objects.filter(site_id=site_id, parent_page=None) 

527 if self.instance: 527 ↛ 530line 527 didn't jump to line 530 because the condition on line 527 was always true

528 pages = pages.exclude(id=self.instance.id) 

529 

530 choices = [(p.id, p.menu_title_en) for p in pages if p.menu_title_en] 

531 self.fields["parent_page"].choices = sorted( 

532 choices + [(None, "---------")], key=lambda x: x[1] 

533 ) 

534 

535 self.fields["menu_title_en"].widget.attrs.update({"class": "menu_title"}) 

536 self.fields["menu_title_fr"].widget.attrs.update({"class": "menu_title"}) 

537 

538 if is_site_en_only(site_id): 538 ↛ 539line 538 didn't jump to line 539 because the condition on line 538 was never true

539 self.fields.pop("content_fr") 

540 self.fields.pop("menu_title_fr") 

541 self.fields.pop("slug_fr") 

542 elif is_site_fr_only(site_id): 542 ↛ 543line 542 didn't jump to line 543 because the condition on line 542 was never true

543 self.fields.pop("content_en") 

544 self.fields.pop("menu_title_en") 

545 self.fields.pop("slug_en") 

546 

547 def save_model(self, request, obj, form, change): 

548 obj.site_id = form.cleaned_data["site_id"] 

549 super().save_model(request, obj, form, change) 

550 

551 

552class NewsForm(forms.ModelForm): 

553 class Meta: 

554 model = News 

555 fields = [ 

556 "title_en", 

557 "title_fr", 

558 "content_en", 

559 "content_fr", 

560 "site_id", 

561 ] 

562 

563 def __init__(self, *args, **kwargs): 

564 site_id = kwargs.pop("site_id") 

565 user = kwargs.pop("user") 

566 super().__init__(*args, **kwargs) 

567 

568 self.fields["site_id"].initial = site_id 

569 

570 if not user.is_staff: 

571 for field_name in ["site_id"]: 

572 field = self.fields[field_name] 

573 # Hide the field is not enough, otherwise BaseForm._clean_fields will not get the value 

574 field.disabled = True 

575 field.widget = field.hidden_widget() 

576 

577 colid = get_collection_id(int(site_id)) 

578 

579 # By default, CKEditor stores files in 1 folder 

580 # We want to store the files in a @colid folder 

581 for field_name in ["content_en", "content_fr"]: 

582 field = self.fields[field_name] 

583 widget = field.widget 

584 widget.config["filebrowserUploadUrl"] = "/ckeditor/upload/" + colid 

585 widget.config["filebrowserBrowseUrl"] = "/ckeditor/browse/" + colid 

586 

587 if is_site_en_only(site_id): 

588 self.fields.pop("content_fr") 

589 self.fields.pop("title_fr") 

590 elif is_site_fr_only(site_id): 

591 self.fields.pop("content_en") 

592 self.fields.pop("title_en") 

593 

594 def save_model(self, request, obj, form, change): 

595 obj.site_id = form.cleaned_data["site_id"] 

596 super().save_model(request, obj, form, change) 

597 

598 

599class InviteUserForm(forms.Form): 

600 """Base form to invite user.""" 

601 

602 required_css_class = "required" 

603 

604 first_name = forms.CharField(label="First name", max_length=150, required=True) 

605 last_name = forms.CharField(label="Last name", max_length=150, required=True) 

606 email = forms.EmailField(label="E-mail address", required=True) 

607 

608 

609class InvitationAdminChangeForm(forms.ModelForm): 

610 class Meta: 

611 model = Invitation 

612 fields = "__all__" 

613 

614 def clean_extra_data(self): 

615 """ 

616 Enforce the JSON structure with the InvitationExtraData dataclass interface. 

617 """ 

618 try: 

619 InvitationExtraData(**self.cleaned_data["extra_data"]) 

620 except Exception as e: 

621 raise forms.ValidationError(e) 

622 

623 return self.cleaned_data["extra_data"] 

624 

625 

626class InvitationAdminAddForm(InvitationAdminChangeForm, CleanEmailMixin): 

627 class Meta: 

628 fields = ("email", "first_name", "last_name", "extra_data") 

629 

630 def save(self, *args, **kwargs): 

631 """ 

632 Populate the invitation data, save in DB and send the invitation e-mail. 

633 """ 

634 cleaned_data = self.clean() 

635 email = cleaned_data["email"] 

636 params = {"email": email} 

637 if cleaned_data.get("inviter"): 

638 params["inviter"] = cleaned_data["inviter"] 

639 else: 

640 user = getattr(self, "user", None) 

641 if isinstance(user, get_user_model()): 

642 params["inviter"] = user 

643 instance = Invitation.create(**params) 

644 instance.first_name = cleaned_data["first_name"] 

645 instance.last_name = cleaned_data["last_name"] 

646 instance.extra_data = cleaned_data.get("extra_data", {}) 

647 instance.save() 

648 full_name = f"{instance.first_name} {instance.last_name}" 

649 instance.send_invitation(self.request, **{"full_name": full_name}) 

650 super().save(*args, **kwargs) 

651 return instance 

652 

653 

654class SignupForm(BaseSignupForm): 

655 email = forms.EmailField(widget=forms.HiddenInput())