Skip to content

Commit

Permalink
Merge pull request #15 from xjlin0/new_attendee
Browse files Browse the repository at this point in the history
Create New attendee outside of family is now possible for certain groups
  • Loading branch information
xjlin0 authored Jul 3, 2021
2 parents 4b13383 + 6911088 commit a119699
Show file tree
Hide file tree
Showing 14 changed files with 334 additions and 114 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ https://dbdiagram.io/d/5d5ff66eced98361d6dddc48
- [x] Permission controlled blocks in single attendee update page, i.e. different blocks/user-settings for different groups
- [x] Generic models such as Note, Place, Past need to have organization column instead of infos
- [x] Add Past as Note
- [ ] Create new instance of Attendee & attending update page with params with meet
- [x] Create new instance of Attendee & attending update page with params with meet
- [ ] delete function for human error
- [x] Modify Attendee save method to combine/convert names by OpenCC to support searches in different text encoding, and retire db level full_name.
- [x] implement secret/private relation/past general
- [ ] delete note/relationship/related_attendee for human error?
- [ ] Move single attendee update page out of data assembly
- [ ] Gathering list (new design with server side processing)
- [ ] Attendance list (new design with server side processing)
Expand Down
2 changes: 1 addition & 1 deletion attendees/persons/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class PastAdmin(admin.ModelAdmin):

def get_queryset(self, request):
qs = super().get_queryset(request)
# counseling_category = Category.objects.get(type='note', display_name=Past.COUNSELING)
counseling_category = Category.objects.get(type='note', display_name=Past.COUNSELING)

if request.resolver_match.func.__name__ == 'changelist_view':
messages.warning(request, 'Not all, but only those records accessible to you will be listed here.')
Expand Down
22 changes: 1 addition & 21 deletions attendees/persons/serializers/attendee_minimal_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,7 @@ def create(self, validated_data):
Create and return a new `Attendee` instance, given the validated data.
"""

attendee_id = self._kwargs['data'].get('attendee-id')
deleting_photo = self._kwargs['data'].get('photo-clear', None)

instance = Attendee.objects.get(pk=attendee_id)
if instance:
if deleting_photo or validated_data.get('photo', None):
old_photo = instance.photo
if old_photo:
old_file = Path(old_photo.path)
old_file.unlink(missing_ok=True)
if deleting_photo:
validated_data['photo'] = None

obj, created = Attendee.objects.update_or_create(
id=attendee_id,
defaults=validated_data,
)

return obj
else:
return None
return Attendee.objects.create(**validated_data)

def update(self, instance, validated_data):
"""
Expand Down
5 changes: 5 additions & 0 deletions attendees/persons/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@
view=datagrid_attendee_update_view,
name='datagrid_attendee_update_view',
),
path(
'<slug:division_slug>/<slug:assembly_slug>/datagrid_attendee_update_view/new',
view=datagrid_attendee_update_view,
name='datagrid_attendee_create_view', # for permission
),
path(
'<slug:division_slug>/<slug:assembly_slug>/datagrid_attendee_update_view/<str:attendee_id>',
view=datagrid_attendee_update_view,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def get_context_data(self, **kwargs):
family_attendances_menu = Menu.objects.filter(url_name='datagrid_user_organization_attendances').first()
available_meets = Meet.objects.filter(assembly__slug=current_assembly_slug).order_by('id')
available_characters = Character.objects.filter(assembly__slug=current_assembly_slug).order_by('display_order')
allowed_to_create_attendee = Menu.user_can_create_attendee(self.request.user)
context.update({
'current_organization_slug': current_organization_slug,
'current_division_slug': current_division_slug,
Expand All @@ -39,6 +40,8 @@ def get_context_data(self, **kwargs):
'available_meets_json': dumps([model_to_dict(m, fields=('id', 'slug', 'display_name')) for m in available_meets]),
'available_characters': available_characters,
'available_characters_json': dumps([model_to_dict(c, fields=('slug', 'display_name')) for c in available_characters]),
'allowed_to_create_attendee': allowed_to_create_attendee,
'create_attendee_urn': f'/persons/{current_division_slug}/{current_assembly_slug}/datagrid_attendee_update_view/new',
})
return context

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from attendees.occasions.models import Assembly
from attendees.persons.models import Attendee, Family
from attendees.users.authorization import RouteAndSpyGuard
from attendees.users.models import Menu
from attendees.utils.view_helpers import get_object_or_delayed_403


Expand All @@ -30,11 +31,13 @@ def get_context_data(self, **kwargs):
current_organization_slug = self.kwargs.get('organization_slug', None)
current_assembly_slug = self.kwargs.get('assembly_slug', 'cfcch_unspecified')
current_assembly_id = Assembly.objects.get(slug=current_assembly_slug).id
targeting_attendee_id = self.kwargs.get('attendee_id', self.request.user.attendee_uuid_str())
targeting_attendee_id = 'new' if self.request.resolver_match.url_name == Menu.CREATE_VIEW_NAME else self.kwargs.get('attendee_id', self.request.user.attendee_uuid_str()) # if more logic needed when create new, a new view will be better
allowed_to_create_attendee = False if self.request.resolver_match.url_name == Menu.CREATE_VIEW_NAME else Menu.user_can_create_attendee(self.request.user)
context.update({
'attendee_contenttype_id': ContentType.objects.get_for_model(Attendee).id,
'family_contenttype_id': ContentType.objects.get_for_model(Family).id,
'empty_image_link': f"{settings.STATIC_URL}images/empty.png",
'allowed_to_create_attendee': allowed_to_create_attendee,
'characters_endpoint': '/occasions/api/user_assembly_characters/',
'meets_endpoint': '/occasions/api/user_assembly_meets/',
'attendingmeets_endpoint': '/persons/api/datagrid_data_attendingmeet/',
Expand Down
158 changes: 120 additions & 38 deletions attendees/static/js/persons/datagrid_attendee_update_view.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
Attendees.datagridUpdate = {
attendeeMainDxForm: null, // will be assigned later, may not needed if use native form.submit()?
attendeeMainDxFormDefault: {
infos: {
names: {},
contacts: {}
}
},
attendeeAttrs: null, // will be assigned later
attendeeId: '', // the attendee is being edited, since it maybe admin/parent editing another attendee
attendeeAjaxUrl: null,
Expand Down Expand Up @@ -41,7 +47,7 @@ Attendees.datagridUpdate = {

init: () => {
console.log("/static/js/persons/datagrid_attendee_update_view.js");
Attendees.datagridUpdate.displayNotifiers();
Attendees.datagridUpdate.displayNotifierFromSearchParam('success');
Attendees.datagridUpdate.initAttendeeForm();
},

Expand Down Expand Up @@ -95,21 +101,19 @@ Attendees.datagridUpdate = {
Attendees.datagridUpdate.attendingMeetDatagrid && Attendees.datagridUpdate.attendingMeetDatagrid.option("editing", {...cellEditingArgs, ...Attendees.datagridUpdate.attendingMeetEditingArgs});
},

displayNotifiers: () => {
const params = new URLSearchParams(location.search);
if (params.has('success')) {
displayNotifierFromSearchParam: (successParamName) => {
const successParamValue = Attendees.utilities.extractParamAndReplaceHistory(successParamName);
if (successParamValue) {
DevExpress.ui.notify(
{
message: params.get('success'),
message: successParamValue,
width: 500,
position: {
my: 'center',
at: 'center',
of: window,
}
}, "success", 2500);
params.delete('success');
history.replaceState(null, '', '?' + params + location.hash);
}, 'success', 2500);
}
},

Expand All @@ -122,36 +126,42 @@ Attendees.datagridUpdate = {
Attendees.datagridUpdate.attendeeUrn = Attendees.datagridUpdate.attendeeAttrs.attendeeUrn;
Attendees.datagridUpdate.attendeeId = document.querySelector('input[name="attendee-id"]').value;
// Attendees.datagridUpdate.placeDefaults.object_id = Attendees.datagridUpdate.attendeeId;
Attendees.datagridUpdate.attendeeAjaxUrl = Attendees.datagridUpdate.attendeeAttrs.dataset.attendeeEndpoint + Attendees.datagridUpdate.attendeeId + '/';
$.ajaxSetup({
headers: {
"X-CSRFToken": document.querySelector('input[name="csrfmiddlewaretoken"]').value,
"X-Target-Attendee-Id": Attendees.datagridUpdate.attendeeId,
}
});
$.ajax({
url: Attendees.datagridUpdate.attendeeAjaxUrl,
success: (response) => {
Attendees.datagridUpdate.attendeeFormConfigs = Attendees.datagridUpdate.getAttendeeFormConfigs();
Attendees.datagridUpdate.attendeeFormConfigs.formData = response ? response : {
infos: {
names: {},
contacts: {}
}
};
$('h3.page-title').text('Details of ' + Attendees.datagridUpdate.attendeeFormConfigs.formData.infos.names.original);
window.top.document.title = Attendees.datagridUpdate.attendeeFormConfigs.formData.infos.names.original;
Attendees.datagridUpdate.attendeeMainDxForm = $("div.datagrid-attendee-update").dxForm(Attendees.datagridUpdate.attendeeFormConfigs).dxForm("instance");
Attendees.datagridUpdate.populateBasicInfoBlock();
Attendees.datagridUpdate.initListeners();
Attendees.datagridUpdate.familyAttrDefaults.division = response.division;
Attendees.datagridUpdate.familyAttrDefaults.display_name = response.infos.names.original + ' family';
},
error: (response) => {
console.log('Failed to fetch data in Attendees.datagridUpdate.initAttendeeForm(), error: ', response);
},
});

if (Attendees.datagridUpdate.attendeeId === 'new') {
Attendees.datagridUpdate.attendeeAjaxUrl = Attendees.datagridUpdate.attendeeAttrs.dataset.attendeeEndpoint;
$('h3.page-title').text('New Attendee: more data can be entered after save');
window.top.document.title = 'New Attendee';
Attendees.utilities.editingEnabled = true;
Attendees.datagridUpdate.attendeeFormConfigs = Attendees.datagridUpdate.getAttendeeFormConfigs();
Attendees.datagridUpdate.attendeeMainDxForm = $("div.datagrid-attendee-update").dxForm(Attendees.datagridUpdate.attendeeFormConfigs).dxForm("instance");
Attendees.datagridUpdate.attendeeFormConfigs.formData = Attendees.datagridUpdate.attendeeMainDxFormDefault;
Attendees.datagridUpdate.populateBasicInfoBlock({});
} else {
Attendees.datagridUpdate.attendeeAjaxUrl = Attendees.datagridUpdate.attendeeAttrs.dataset.attendeeEndpoint + Attendees.datagridUpdate.attendeeId + '/';
$.ajax({
url: Attendees.datagridUpdate.attendeeAjaxUrl,
success: (response) => {
Attendees.datagridUpdate.attendeeFormConfigs = Attendees.datagridUpdate.getAttendeeFormConfigs();
Attendees.datagridUpdate.attendeeFormConfigs.formData = response ? response : Attendees.datagridUpdate.attendeeMainDxFormDefault;
$('h3.page-title').text('Details of ' + Attendees.datagridUpdate.attendeeFormConfigs.formData.infos.names.original);
window.top.document.title = Attendees.datagridUpdate.attendeeFormConfigs.formData.infos.names.original;
Attendees.datagridUpdate.attendeeMainDxForm = $("div.datagrid-attendee-update").dxForm(Attendees.datagridUpdate.attendeeFormConfigs).dxForm("instance");
Attendees.datagridUpdate.populateBasicInfoBlock();
Attendees.datagridUpdate.initListeners();
Attendees.datagridUpdate.familyAttrDefaults.division = response.division;
Attendees.datagridUpdate.familyAttrDefaults.display_name = response.infos.names.original + ' family';
},
error: (response) => {
console.log('Failed to fetch data in Attendees.datagridUpdate.initAttendeeForm(), error: ', response);
},
});
}
},

attachContactAddButton: () => {
Expand All @@ -174,7 +184,7 @@ Attendees.datagridUpdate = {
},

getAttendeeFormConfigs: () => { // this is the place to control blocks of AttendeeForm
const originalItems = [
const basicItems = [
{
colSpan: 4,
itemType: "group",
Expand Down Expand Up @@ -256,6 +266,9 @@ Attendees.datagridUpdate = {
caption: "Basic info. Fields after nick name can be removed by clearing & save.", // adding element in caption by $("<span>", {text:"hi 5"}).appendTo($("span.dx-form-group-caption")[1])
items: [], // will populate later for dynamic contacts
},
];

const moreItems = [
{
colSpan: 24,
colCount: 24,
Expand Down Expand Up @@ -538,6 +551,9 @@ Attendees.datagridUpdate = {
},
],
},
];

const buttonItems = [
{ // https://supportcenter.devexpress.com/ticket/details/t681806
itemType: "button",
name: "mainAttendeeFormSubmit",
Expand All @@ -551,9 +567,9 @@ Attendees.datagridUpdate = {
icon: "save",
hint: "save attendee data in the page",
type: "default",
useSubmitBehavior: false,
useSubmitBehavior: true,
onClick: (e) => {
if (confirm("Are you sure?")) {
if (Attendees.datagridUpdate.attendeeMainDxForm.validate().isValid && confirm('Are you sure?')) {

const userData = new FormData($('form#attendee-update-form')[0]);
if (!$('input[name="photo"]')[0].value) {
Expand All @@ -562,20 +578,24 @@ Attendees.datagridUpdate = {
const userInfos = Attendees.datagridUpdate.attendeeFormConfigs.formData.infos;
userInfos['contacts'] = Attendees.utilities.trimBothKeyAndValueButKeepBasicContacts(userInfos.contacts); // remove emptied contacts
userData.set('infos', JSON.stringify(userInfos));
// userData._method = userData.id ? 'PUT' : 'POST';

$.ajax({
url: Attendees.datagridUpdate.attendeeAjaxUrl,
contentType: false,
processData: false,
dataType: 'json',
data: userData,
method: Attendees.datagridUpdate.attendeeId ? 'PUT' : 'POST',
method: Attendees.datagridUpdate.attendeeId && Attendees.datagridUpdate.attendeeId !== 'new' ? 'PUT' : 'POST',
success: (response) => { // Todo: update photo link, temporarily reload to bypass the requirement
console.log("success here is response: ", response);
const parser = new URL(window.location);
parser.searchParams.set('success', 'Saving attendee success');
window.location = parser.href;

if (parser.href.split('/').pop().startsWith('new')){
const newAttendeeIdUrl = '/' + response.id;
window.location = parser.href.replace('/new', newAttendeeIdUrl);
}else {
window.location = parser.href;
}
},
error: (response) => {
console.log('Failed to save data for main AttendeeForm, error: ', response);
Expand All @@ -597,12 +617,19 @@ Attendees.datagridUpdate = {
},
},
];

const originalItems = [...basicItems, ...(Attendees.datagridUpdate.attendeeId === 'new' ? [] : moreItems ), ...buttonItems];

return {
showValidationSummary: true,
readOnly: !Attendees.utilities.editingEnabled,
onContentReady: () => {
$('div.spinner-border').hide();
Attendees.utilities.toggleDxFormGroups();
},
onFieldDataChanged: (e) => {
Attendees.datagridUpdate.attendeeMainDxForm.validate();
},
colCount: 24,
formData: null, // will be fetched
items: originalItems.filter(item => {
Expand All @@ -611,6 +638,11 @@ Attendees.datagridUpdate = {
};
},

attendeeNameValidator: () => {
const attendeeFromData = Attendees.datagridUpdate.attendeeMainDxForm.option('formData');
return attendeeFromData.first_name || attendeeFromData.last_name || attendeeFromData.first_name2 || attendeeFromData.last_name2;
},

familyButtonFactory: (attrs) => {
return $('<button>', {
type: 'button',
Expand Down Expand Up @@ -660,13 +692,39 @@ Attendees.datagridUpdate = {
editorOptions: {
placeholder: 'English',
},
validationRules: [
{
type: 'custom',
reevaluate: true,
validationCallback: Attendees.datagridUpdate.attendeeNameValidator,
message: 'first or last name is required'
},
{
type: "stringLength",
reevaluate: true,
max: 25,
message: "No more than 25 characters"
}
],
},
{
colSpan: 7,
dataField: 'last_name',
editorOptions: {
placeholder: 'English',
},
validationRules: [
{
type: 'custom',
validationCallback: Attendees.datagridUpdate.attendeeNameValidator,
message: 'first or last name is required'
},
{
type: "stringLength",
max: 25,
message: "No more than 25 characters"
}
],
},
{
colSpan: 7,
Expand Down Expand Up @@ -698,10 +756,34 @@ Attendees.datagridUpdate = {
{
colSpan: 7,
dataField: 'last_name2',
validationRules: [
{
type: 'custom',
validationCallback: Attendees.datagridUpdate.attendeeNameValidator,
message: 'first or last name is required'
},
{
type: "stringLength",
max: 8,
message: "No more than 8 characters"
}
],
},
{
colSpan: 7,
dataField: 'first_name2',
validationRules: [
{
type: 'custom',
validationCallback: Attendees.datagridUpdate.attendeeNameValidator,
message: 'first or last name is required'
},
{
type: "stringLength",
max: 12,
message: "No more than 12 characters"
}
],
},
{
colSpan: 7,
Expand Down
Loading

0 comments on commit a119699

Please sign in to comment.