Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create New attendee outside of family is now possible for certain groups #15

Merged
merged 6 commits into from
Jul 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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