Show member name on register form

I have a register form to log attendance at a training session, with models:

class Member(models.Model):

	id = models.BigAutoField(primary_key=True)
	account = models.ForeignKey('Account', verbose_name=_('account'), on_delete=models.CASCADE)
	first_name = models.CharField(_('first name'), max_length=32) #, null=True, blank=True
	middle_name = models.CharField(_('middle name(s)'), max_length=64, null=True, blank=True)
	last_name = models.CharField(_('last name'), max_length=32) #, null=True, blank=True
	known_as = models.CharField(_('known as'), max_length=32, null=True, blank=True)
	…
	
	def member_display_name(self):
		return_name = str(self.first_name) + ' '
		if self.known_as:
			return_name += '(' + str(self.known_as) + ') '
		return_name += str(self.last_name)
		return return_name
	
	def __str__(self):
		return self.member_display_name()

class Register(models.Model):
	id = models.BigAutoField(primary_key=True)
	member = models.ManyToManyField('Member', through='Member_Register')
	regdate = models.DateField(_('Date'), null=False, default=datetime.now)
	regclass = models.CharField(_('Group'), max_length=64, null=False, blank=False)
	…

class Member_Register(models.Model):
	id = models.BigAutoField(primary_key=True)
	member = models.ForeignKey(Member, null=True, on_delete=models.CASCADE)
	register = models.ForeignKey(Register, verbose_name=_('register'), null=True, on_delete=models.CASCADE)
	present = models.BooleanField(_('Present'), null=True, blank=True, default=False, db_index=True)
	late = models.BooleanField(_('Late'), null=True, blank=True, default=False, db_index=True)
	
	createdate = models.DateTimeField(auto_now_add=True)
	moddate = models.DateTimeField(auto_now=True)
	
	class Meta:
		constraints = [
			models.UniqueConstraint(fields=['member', 'register'], name='member_register duplicate prevention')
		]

I have this working with a formset. I have three questions:

  1. On the formset I have the fields member, present and late from the Member_Register model, the first defaults to a select and the other two are modified with widgets to be checkboxes. The select has the Member.member_display_name displayed as the option (and the Member.id at the value), enabled via the Member.__str__ override. However, I don’t really need a select here, I just want to show Member.member_display_name as text (not a form element) on the form (with the member.value (ie the Member.id) as a hidden field). I know how to make the hidden field in the formset, but I cannot work out how to display Member.member_display_name – how can I get that data on to the template?

  2. How to sort the formset by Member.last_name?

  3. Is more of a logic query. Because the sessions are somewhat irregular the register is created by the user going to a create register form, where they select the date of the session and the group, submitting this form populates the register with the Members, and redirects to the register. Is this a reasonable way to create such registers?

Thanks

Ok, solved q1:

{{ form.instance.member.member_display_name }}

So any help with q2 and 3 much appreciated.

It’s going to be easiest to answer this in the context of the view creating and using that formset.

Could be. Hard to tell without seeing the actual code.

At the moment I have not completed/tested the creation of the register, but I have a test register for updating, with the following view:

class ControlRegisterView(UpdateView):
	template_name = 'control/register_edit.html'
	form_class = ControlRegisterForm
	#formset = ControlRegisterLine
	model = Register
	
	def get_context_data(self, **kwargs):
		data = super(ControlRegisterView, self).get_context_data(**kwargs)
		if self.request.POST:
			data['member_list'] = ControlRegisterLineFormset(self.request.POST, instance=self.object)
		else:
			data['member_list'] = ControlRegisterLineFormset(instance=self.object)
		return data
		
	def form_valid(self, form):
		context = self.get_context_data()
		member_list = context['member_list']
		with transaction.atomic():
			form.instance.created_by = self.request.user
			self.object = form.save()
			if member_list.is_valid():
				member_list.instance = self.object
				member_list.save()
			else:
				print('member_list form not valid')
				messages.warning(self.request, "{} save failed".format(member_list))

		return super(ControlRegisterView, self).form_valid(form)
		
	def get_success_url(self):
		return reverse_lazy('control-register-update', kwargs={'pk': self.kwargs['pk']})

Hmmm…

I’ve never tried to use formsets (or inlineformsets) within the Django generic CBVs. I’ve never considered them the right fit for that.

Figuring out what to properly override in that case seems a bit much for what benefit you might gain.

For example:

I don’t see the value of having the if self.request.POST condition here. The get_context_data isn’t called on a POST. (Yes, it can get called on an invalid form condition, but at that point, it’s more acting like a GET in that it’s preparing the form to be rerendered with the errors. I’d think more of this belongs in get_form, not here.

The form_valid method is called after the form has already been validated. If get_form creates the formsets, then by the time you get here, you know everything is valid.

Finally, I’m not sure that the inlineformset is an appropriate abstraction for a Many-to-Many relationship. Again, my gut reaction is that it may need to be just a regular formset where you manage the ManyToMany assignment within form_valid.

Ok, I’ll look at some of this again in the light of your comments.

However, I’m no closer to sorting the register.

And, what I’ve also noticed is that calling form.instance.member.member_display_name initiates an SQL query for each row of the register – which increases the number of queries on the form from 6 up to 30–40 (potentially this could be in the hundreds). This seems like an unecessary load when all the information needed can be selected in one query (with a join).

select * from member_register where register = "n"
join member on member_register.member = member.id
order by member.last_name ;

I get my sort for free too :wink:

So I am also wondering if there are more efficient ways to get the member_register/member data for the formset? Which also begs the question as to whether Member.member_display_name is an appropriate method for what is essentially string manipulation?

Thanks

Fixed typo!

I don’t see any of your code associated with creating and using the formset. There’s still a lot of this process I don’t think you’ve posted yet.

To appropriately provide advice, I need to see everything that is part of this process.

No, I think I have posted everything I’ve done on this part so far, except the template and the urls.py – which I don’t believe make a difference one way or the other.

At the risk of quoting Donald Rumsfeld, I think we’re getting into the realm of “unknown, unknowns”. That is to say that how this part resolves will affect the way other parts resolve. Currently there is no process for creating the register – I’ve just manually created a database record and added related records to it via the functionality of the inline formset (ie add one additional record at a time). The code for adding and updating the members to the register has already been posted.

I have also outlined (above) how I would intend to populate the register in the futue: a user uses a form to select the date and group for the register, and the process looks up the members of the group, creates Register and populates Member_Register . I expect that there will be a constraint to prevent a group having more than one register per day.

So far I can see that the view fires off an SQL query to get Member_Register records to populate the inline formset, but I can’t work out where that is initiated - it would be trivial to add a sort and join to that query (well, there is already an order by, but that just sorts by Member_Register.id which is next to useless).

The key, at the moment, seems to be to finesse the SQL query in order to sort the formset and get the member name information to display in the list of forms. IMO.

Actually, the template could be very important here - but I can’t know that either way until it’s posted.

Likewise, the definition you’re using for the Formset. There are lots of options available and related functions that can be supplied.

Or, for another example, you make reference to form.instance.member.member_display_name, but your only other reference to that expression is as a template expression, so I have no context to evaluate the significance of that expression.

Whether that expression needs to generate an SQL query for each row does depend upon factors not currently shown. You make reference to having a SQL join involved, but you’re not showing the ORM query originating those objects for me to show you how to implement the join in the ORM. (Or, to allow me to address the fundamental issue as to whether making that reference is even the appropriate object to reference at that point.)

So yes, I can talk in abstracts and have absolutely no confidence that we’re using the same words for the same concepts within the same context.

Or, you can post what you’re working with, and we can discuss specific issues in the context that you’re being faced with.

form.instance.member.member_display_name is Member.member_display_name which is shown in the first post.

Here are the relevant forms:

class ControlRegisterForm(forms.ModelForm):

	class Meta:
		model = Register
		
		fields = (
			'regdate',
			'regclass',
			'regnote',
			)

class ControlRegisterLineForm(forms.ModelForm):

	class Meta:
		model = Member_Register
		
		fields = (
			'member',
			'present',
			'late',
			)
		
		widgets = {
			'present': forms.CheckboxInput,
			'late': forms.CheckboxInput,
			#'member': forms.HiddenInput(),
			'member': forms.TextInput(),
		}
			


ControlRegisterLineFormset = forms.models.inlineformset_factory(Register, Member_Register,
									form = ControlRegisterLineForm,
									can_delete=False,
									#max_num=25,
									extra=1,)

The member element in ControlRegisterLineForm should be hidden, I am just switching it back and forth to get some feedback as to what is being returned, etc.

My reference to a join is speculative, in that if I manually assembled the query I’d have all the data I needed in one query. I have literally no idea how the query used to get the formset rows is triggered/assembled, I can only see the query that is made.

The query that is used is:

    SELECT `b_manage_member_register`.`id`,
           `b_manage_member_register`.`member_id`,
           `b_manage_member_register`.`register_id`,
           `b_manage_member_register`.`present`,
           `b_manage_member_register`.`late`,
           `b_manage_member_register`.`createdate`,
           `b_manage_member_register`.`moddate`
      FROM `b_manage_member_register`
     WHERE `b_manage_member_register`.`register_id` = 2
     ORDER BY `b_manage_member_register`.`id` ASC

So my speculation is hence how to change that query to have one query to get all the data I need, in the order I need it:

    SELECT `b_manage_member_register`.`id`,
           `b_manage_member_register`.`member_id`,
           `b_manage_member_register`.`register_id`,
           `b_manage_member_register`.`present`,
           `b_manage_member_register`.`late`,
           `b_manage_member_register`.`createdate`,
           `b_manage_member_register`.`moddate`,
           `b_manage_register`.`first_name`,
           `b_manage_register`.`last_name`,
           `b_manage_register`.`known_as`,
      FROM `b_manage_member_register`
     JOIN `b_manage_member` on `b_manage_member_register`.`id` = `b_manage_member`.`id`
     WHERE `b_manage_member_register`.`register_id` = 2
     ORDER BY `b_manage_member`.`last_name` ASC

Thus the output of Member.member_display_name can be formed via text manipulation on the template.

I am not sure the template is very useful, as I am currently hacking it about, but:

{% extends "b_manage/control/_base.html" %}

{% load i18n l10n phonenumber_tags bjsc_manage bootstrap4 %}

{% block title %}{% trans "Edit register" %}{% endblock title %}


{% block body %}


<h5><a href="/control/">Control home</a> &gt; 

{% if member.account_id %}
	<a href="{% url 'control-account-update' pk=member.account_id %}">Account</a>
{% elif view.kwargs.account_id %}
	<a href="{% url 'control-account-update' pk=view.kwargs.account_id %}">Account</a>
{% endif %}

</h5>


	<h3>Register</h3>



 <form  class="form-horizontal" method="post" role="form">
 
        {{ member_list.management_form }}

      
        {% csrf_token %}
        
        {% bootstrap_form form layout='horizontal' %}
                
 		<div class="form-group row">
            <div class=" col-sm-2 col-lg-2 ">
			</div>
			<div class=" col-sm-10 col-lg-10">
				<div class="btn-toolbar">
				
				
				<table width="100%" cellspacing=5 cellpadding=5 border=1>
				
				<tr valign="top">
									
										<th>Name</th>
										<th>Present</th>
										        					<td>&nbsp;&nbsp;</td>

										<th>Late</th>

									</tr>
								
				{% for form in member_list %}
				
				
									
				
					<tr valign="top">
					
					<!--oldform-->

							<td width="100%"><div class="form-group row"><label class="col-sm-6 col-lg-6 col-form-label" for="id_member_register_set-{{ forloop.counter0 }}-member"><!-- form.instance.member.member_display_name --></label></div>
														
									{% bootstrap_field form.member layout='horizontal' show_label=False %}							
							
							</td>
        
        					<td align="center"><input type="checkbox" name="member_register_set-{{forloop.counter0}}-present" class="form-check-input" id="id_member_register_set-{{forloop.counter0}}-present" {% if form.present.value %}checked{% endif %}></td>
        					<td></td>
        					<td align="center"><input type="checkbox" name="member_register_set-{{forloop.counter0}}-late" class="form-check-input" id="id_member_register_set-{{forloop.counter0}}-late" {% if form.late.value %}checked{% endif %}></td>
           
        {% if not forloop.last %}
							
				<input type="hidden" name="{{ form.id.html_name }}" value="{{ form.id.value }}" id="{{ form.id.id_for_label }}">
				
		{% else %}
				
				<input type="hidden" name="{{ form.id.html_name }}" value="" id="{{ form.id.id_for_label }}">

		{% endif %}

							
							<input type="hidden" name="{{ form.register.html_name }}" value="{{ form.register.value }}" id="{{ form.register.id_for_label }}">

							
        
    					<!--oldformend-->
    
					</tr>		


<!--newform-->

        
<!--endnewform-->
        
        
        
        <!--
        <input type="hidden" name="{{ choicesform.directorylink.html_name }}" value="" placeholder="" id="{{ choicesform.directorylink.id_for_label }}">
-->

						
					{% endfor %}
					
					
				</table>
				
				
				

					<button type="submit" class="btn btn-primary">Save</button>
					{% if member.account_id %}
						<a href="{% url 'control-account-update' pk=member.account_id %}" class="btn btn-default">Cancel</a>
					{% elif view.kwargs.account_id %}
						<a href="{% url 'control-account-update' pk=view.kwargs.account_id %}" class="btn btn-default">Cancel</a>
					{% endif %}
					
					<div class="float-right">
						{% if member.id %}<a href="{% url 'control-member-confirm-delete' pk=member.id %}" class="btn btn-danger">Delete member</a>{% endif %}
					</div>
					
					
					
					
					
					
				</div>
			</div>
		</div>
		
</form>
      

{% endblock %}

Ok, the picture has become a lot clearer.

When you are creating the formset:
data['member_list'] = ControlRegisterLineFormset(instance=self.object)

You have an additional queryset parameter that you can pass for the related objects.

For example, you should be able to do something like this:
data['member_list'] = ControlRegisterLineFormset(instance=self.object, queryset=Member_Register.objects.select_related('member').order_by('member__last_name'))

This will generate the join to reduce the number of queries being issued when rendering the formset along with sequencing the members by their last name.

I think this addresses the fundamental issues you’ve raised.

Regarding your last question as to your process being reasonable, that’s a “business rule” decision. If every session is supposed to be attended by every member (or at least a majority of each), then yes I’d say it’s quite reasonable.

Side note: I’ve currently got 14 projects in my work directory where formsets are being used. All 14 use Django generic CBVs except for the views working with formsets. Three of them use a couple of in-house developed CBVs designed to work with multiple forms and formsets, the other 11 use FBVs.
Given how formsets are used, I don’t believe that the Django CBVs are the right fit for them. I do think your logic is going to be cleaner and more understandable by not using an UpdateView.

Thank you! I was looking at the code again last night, and was thinking that there must be a way to put the parameters in that line, but was thinking that would just be an order_by. Looking at the documentation didn’t help much on this, until I realised that “it’s a form”, so is going to take a queryset!

So, that definitely managed the sort, just had to modify a little order_by('member__last_name', 'member__first_name').

The problem I am struggling with from there is how to get member.last_name etc, to display on the form (effectively as a label, rather than data input). I’ve tried to go through the member_list object, but can’t really see where that data might be, though the data appears to be in the query:

    SELECT `b_manage_member_register`.`id`,
           …
           `b_manage_member`.`first_name`,
           `b_manage_member`.`middle_name`,
           `b_manage_member`.`last_name`,
           `b_manage_member`.`known_as`,
           …
      FROM `b_manage_member_register`
      LEFT OUTER JOIN `b_manage_member`
        ON (`b_manage_member_register`.`member_id` = `b_manage_member`.`id`)
     WHERE `b_manage_member_register`.`register_id` = 2
     ORDER BY `b_manage_member`.`last_name` ASC, `b_manage_member`.`first_name` ASC

After thinking about it, it is probably better that the display name is formatted with a filter, as there is no point going off to the database again to get that information.

With regard to populating the register, there seem to be two choices: pre-populate the register with members who may or not be there or let the person taking the register populate the register with those that actually are there.

While the first method may produce meny rows that are actually useless (in terms of recording nothing, other than someone not being there, which is deducible from the fact that they are not marked as “present”), the second method is relatively slow and tedious if there are more than a handfull of members present.

On balance I think it is better to go for the first option, but there could be an option in the creation method to not populate the register.

A third option is to populate the register, and then allow the register taker to delete rows for those not present – although that’s not really an option I want to get in to.

I take your point about using CBVs with formsets. The trouble being that when researching the problem the solutions one finds tend to be CBV-based. But it is worth considering how that moves forward – at the moment I just want something functioning, finessing it can come later (or maybe not :grinning: ).

You’ve got at least two options:

  • Define it as a field with the disabled attribute. It’ll show up as an input widget of the appropriate type, but greyed out and unusable.

  • Yes, you can refer to the field using the dotted notation within the form as you’ve identified above. Since you’re using the select_related clause on the queryset, it’s not going to generate an additional query.

Aha, super!

It works out somewhat serendipitiously, as I can just reference form.instance.member and that gives me the formatted name because I’d overridden __str__ in the model.

Thanks