def create_uploaded_persons_tasks(data): """ Create persons and tasks from upload data. """ # Quick sanity check. if any([row.get('errors') for row in data]): raise InternalError('Uploaded data contains errors, cancelling upload') persons_created = [] tasks_created = [] events = set() with transaction.atomic(): for row in data: try: fields = {key: row[key] for key in Person.PERSON_UPLOAD_FIELDS} fields['username'] = create_username(row['personal'], row['family']) if fields['email']: # we should use existing Person or create one p, created = Person.objects.get_or_create( email__iexact=fields['email'], defaults=fields ) if created: persons_created.append(p) else: # we should create a new Person without any email provided p = Person(**fields) p.save() persons_created.append(p) if row['event'] and row['role']: e = Event.objects.get(slug=row['event']) r = Role.objects.get(name=row['role']) # is the number of learners attending the event changed, # we should update ``event.attendance`` if row['role'] == 'learner': events.add(e) t, created = Task.objects.get_or_create(person=p, event=e, role=r) if created: tasks_created.append(t) except IntegrityError as e: raise IntegrityError('{0} (for {1})'.format(str(e), row)) except ObjectDoesNotExist as e: raise ObjectDoesNotExist('{0} (for {1})'.format(str(e), row)) for event in events: # if event.attendance is lower than number of learners, then # update the attendance update_event_attendance_from_tasks(event) return persons_created, tasks_created
def create_uploaded_persons_tasks(data): """ Create persons and tasks from upload data. """ # Quick sanity check. if any([row.get('errors') for row in data]): raise InternalError('Uploaded data contains errors, cancelling upload') persons_created = [] tasks_created = [] events = set() with transaction.atomic(): for row in data: try: fields = {key: row[key] for key in Person.PERSON_UPLOAD_FIELDS} fields['username'] = row['username'] if fields['email']: # we should use existing Person or create one p, created = Person.objects.get_or_create( email__iexact=fields['email'], defaults=fields ) if created: persons_created.append(p) else: # we should create a new Person without any email provided p = Person(**fields) p.save() persons_created.append(p) if row['event'] and row['role']: e = Event.objects.get(slug=row['event']) r = Role.objects.get(name=row['role']) # is the number of learners attending the event changed, # we should update ``event.attendance`` if row['role'] == 'learner': events.add(e) t, created = Task.objects.get_or_create(person=p, event=e, role=r) if created: tasks_created.append(t) except IntegrityError as e: raise IntegrityError('{0} (for {1})'.format(str(e), row)) except ObjectDoesNotExist as e: raise ObjectDoesNotExist('{0} (for {1})'.format(str(e), row)) for event in events: # if event.attendance is lower than number of learners, then # update the attendance update_event_attendance_from_tasks(event) return persons_created, tasks_created
def create_uploaded_persons_tasks(data, request=None): """ Create persons and tasks from upload data. """ # Quick sanity check. if any([row.get("errors") for row in data]): raise InternalError("Uploaded data contains errors, cancelling upload") persons_created = [] tasks_created = [] events = set() with transaction.atomic(): for row in data: try: row_repr = ("{personal} {family} {username} <{email}>, " "{role} at {event}").format(**row) fields = {key: row[key] for key in Person.PERSON_UPLOAD_FIELDS} fields["username"] = row["username"] if row["person_exists"] and row["existing_person_id"]: # we should use existing Person p = Person.objects.get(pk=row["existing_person_id"]) elif row["person_exists"] and not row["existing_person_id"]: # we should use existing Person p = Person.objects.get( personal=fields["personal"], family=fields["family"], username=fields["username"], email=fields["email"], ) else: # we should create a new Person without any email provided p = Person(**fields) p.save() persons_created.append(p) if row["event"] and row["role"]: e = Event.objects.get(slug=row["event"]) r = Role.objects.get(name=row["role"]) # if the number of learners attending the event changed, # we should update ``event.attendance`` if row["role"] == "learner": events.add(e) t, created = Task.objects.get_or_create(person=p, event=e, role=r) if created: tasks_created.append(t) except IntegrityError as e: raise IntegrityError('{0} (for "{1}")'.format( str(e), row_repr)) except ObjectDoesNotExist as e: raise ObjectDoesNotExist('{0} (for "{1}")'.format( str(e), row_repr)) jobs_created = [] rqjobs_created = [] # for each created task, try to add a new-(supporting)-instructor action with transaction.atomic(): for task in tasks_created: # conditions check out if NewInstructorAction.check(task): objs = dict(task=task, event=task.event) # prepare context and everything and create corresponding RQJob jobs, rqjobs = ActionManageMixin.add( action_class=NewInstructorAction, logger=logger, scheduler=scheduler, triggers=Trigger.objects.filter(active=True, action="new-instructor"), context_objects=objs, object_=task, request=request, ) jobs_created += jobs rqjobs_created += rqjobs # conditions check out if NewSupportingInstructorAction.check(task): objs = dict(task=task, event=task.event) # prepare context and everything and create corresponding RQJob jobs, rqjobs = ActionManageMixin.add( action_class=NewSupportingInstructorAction, logger=logger, scheduler=scheduler, triggers=Trigger.objects.filter( active=True, action="new-supporting-instructor"), context_objects=objs, object_=task, request=request, ) jobs_created += jobs rqjobs_created += rqjobs return persons_created, tasks_created
def create_uploaded_persons_tasks(data): """ Create persons and tasks from upload data. """ # Quick sanity check. if any([row.get('errors') for row in data]): raise InternalError('Uploaded data contains errors, cancelling upload') persons_created = [] tasks_created = [] events = set() with transaction.atomic(): for row in data: try: row_repr = ('{personal} {family} {username} <{email}>, ' '{role} at {event}').format(**row) fields = {key: row[key] for key in Person.PERSON_UPLOAD_FIELDS} fields['username'] = row['username'] if row['person_exists'] and row['existing_person_id']: # we should use existing Person p = Person.objects.get(pk=row['existing_person_id']) elif row['person_exists'] and not row['existing_person_id']: # we should use existing Person p = Person.objects.get( personal=fields['personal'], family=fields['family'], username=fields['username'], email=fields['email'], ) else: # we should create a new Person without any email provided p = Person(**fields) p.save() persons_created.append(p) if row['event'] and row['role']: e = Event.objects.get(slug=row['event']) r = Role.objects.get(name=row['role']) # if the number of learners attending the event changed, # we should update ``event.attendance`` if row['role'] == 'learner': events.add(e) t, created = Task.objects.get_or_create(person=p, event=e, role=r) if created: tasks_created.append(t) except IntegrityError as e: raise IntegrityError('{0} (for "{1}")'.format(str(e), row_repr)) except ObjectDoesNotExist as e: raise ObjectDoesNotExist('{0} (for "{1}")'.format(str(e), row_repr)) return persons_created, tasks_created
class TestExportingPersonData(BaseExportingTest): def setUp(self): # don't remove all badges # super().setUp() # prepare user self.user = Person( username="******", personal="User", family="Primary", email="*****@*****.**", is_active=True, data_privacy_agreement=True, ) self.user.set_password('password') self.user.save() # save API endpoint URL self.url = reverse('api:export-person-data') def login(self): """Overwrite BaseExportingTest's login method: instead of loggin in as an admin, use a normal user.""" self.client.login(username='******', password='******') def prepare_data(self, user): """Populate relational fields for the user.""" # create and set airport for the user airport = Airport.objects.create( iata='DDD', fullname='Airport 55x105', country='CM', latitude=55.0, longitude=105.0, ) self.user.airport = airport self.user.save() # create a fake organization test_host = Organization.objects.create(domain='example.com', fullname='Test Organization') # create an event that will later be used event = Event.objects.create( start=datetime.date(2018, 6, 16), end=datetime.date(2018, 6, 17), slug='2018-06-16-AMY-event', host=test_host, url='http://example.org/2018-06-16-AMY-event', ) # add a role Role.objects.create(name='instructor', verbose_name='Instructor') # add an admin user self.setup_admin() # award user some badges via awards (intermediary model) # one badge was awarded for the event award1 = Award.objects.create( person=self.user, badge=Badge.objects.get(name='swc-instructor'), event=event, awarded=datetime.date(2018, 6, 16), ) # second badge was awarded without any connected event award2 = Award.objects.create( person=self.user, badge=Badge.objects.get(name='dc-instructor'), awarded=datetime.date(2018, 6, 16), ) # user took part in the event as an instructor self.user.task_set.create( event=event, role=Role.objects.get(name='instructor'), ) # user knows a couple of languages self.user.languages.set( Language.objects.filter(name__in=['English', 'French'])) # add training requests training_request = TrainingRequest.objects.create( # mixins data_privacy_agreement=True, code_of_conduct_agreement=True, state='p', # pending person=self.user, group_name='Mosquitos', personal='User', middle='', family='Primary', email='*****@*****.**', github='primary_user', occupation='undisclosed', occupation_other='', affiliation='AMY', location='Worldwide', country='W3', underresourced=False, # need to set it below # domains=KnowledgeDomain.objects.first(), domains_other='E-commerce', underrepresented='', nonprofit_teaching_experience='Voluntary teacher', # need to set it below # previous_involvement=Role.objects.filter(name='instructor'), previous_training='course', previous_training_other='', previous_training_explanation='A course for voluntary teaching', previous_experience='ta', previous_experience_other='', previous_experience_explanation='After the course I became a TA', programming_language_usage_frequency='weekly', teaching_frequency_expectation='monthly', max_travelling_frequency='not-at-all', max_travelling_frequency_other='', reason='I want to became an instructor', comment='I like trains', training_completion_agreement=True, workshop_teaching_agreement=True, notes='Admin notes invisible to the user', ) training_request.domains.set([KnowledgeDomain.objects.first()]) training_request.previous_involvement.set( Role.objects.filter(name='instructor')) # add some training progress TrainingProgress.objects.create( trainee=self.user, requirement=TrainingRequirement.objects.get(name='Discussion'), state='p', # passed event=event, evaluated_by=None, discarded=False, url=None, ) TrainingProgress.objects.create( trainee=self.user, requirement=TrainingRequirement.objects.get(name='DC Homework'), state='f', # failed event=None, evaluated_by=self.admin, discarded=False, url='http://example.org/homework', ) def test_unauthorized_access(self): """Make sure only authenticated users can access.""" # logout self.client.logout() # retrieve endpoint rv = self.client.get(self.url) # make sure it's inaccessible self.assertEqual(rv.status_code, 401) def test_only_for_one_user(self): """Make sure the results are available only for the logged-in user, no-one else.""" # prepare a different user self.second_user = Person( username="******", personal="User", family="Secondary", email="*****@*****.**", is_active=True, data_privacy_agreement=True, ) self.second_user.set_password('password') self.second_user.save() # login as first user self.client.login(username='******', password='******') # retrieve endpoint rv = self.client.get(self.url) self.assertEqual(rv.status_code, 200) # make sure this endpoint returns current user data self.assertEqual(rv.json()['username'], 'primary_user') # login as second user self.client.login(username='******', password='******') rv = self.client.get(self.url) self.assertEqual(rv.status_code, 200) # make sure this endpoint does not return first user data now self.assertEqual(rv.json()['username'], 'secondary_user') def test_all_related_objects_shown(self): """Test if all related fields are present in data output.""" self.login() # retrieve endpoint rv = self.client.get(self.url) self.assertEqual(rv.status_code, 200) # API results parsed as JSON user_data = rv.json() user_data_keys = user_data.keys() # make sure these fields are NOT in the API output missing_fields = [ 'password', 'is_active', 'notes', ] # simple (non-relational) fields expected in API output expected_fields = [ 'data_privacy_agreement', 'personal', 'middle', 'family', 'email', 'username', 'gender', 'may_contact', 'publish_profile', 'github', 'twitter', 'url', 'user_notes', 'affiliation', 'occupation', 'orcid', ] # relational fields expected in API output expected_relational = [ 'airport', 'badges', 'lessons', 'domains', 'languages', 'tasks', # renamed in serializer (was: task_set) 'awards', # renamed in serializer (was: award_set) 'training_requests', # renamed from "trainingrequest_set" 'training_progresses', # renamed from "trainingprogress_set" ] # ensure missing fields are not to be found in API output for field in missing_fields: self.assertNotIn(field, user_data_keys) # ensure required fields are present for field in expected_fields + expected_relational: self.assertIn(field, user_data_keys) def test_relational_fields_structure(self): """Make sure relational fields available via API endpoints retain a specific structure.""" self.prepare_data(user=self.user) self.login() # retrieve endpoint rv = self.client.get(self.url) self.assertEqual(rv.status_code, 200) # API results parsed as JSON data = rv.json() # expected data dict expected = dict() # test expected Airport output expected['airport'] = { 'iata': 'DDD', 'fullname': 'Airport 55x105', 'country': 'CM', 'latitude': 55.0, 'longitude': 105.0, } self.assertEqual(data['airport'], expected['airport']) # test expected Badges output expected['badges'] = [ { 'name': 'swc-instructor', 'title': 'Software Carpentry Instructor', 'criteria': 'Teaching at Software Carpentry workshops or' ' online', }, { 'name': 'dc-instructor', 'title': 'Data Carpentry Instructor', 'criteria': 'Teaching at Data Carpentry workshops or' ' online', }, ] self.assertEqual(data['badges'], expected['badges']) # test expected Awards output expected['awards'] = [ { 'badge': 'swc-instructor', 'awarded': '2018-06-16', 'event': { 'slug': '2018-06-16-AMY-event', 'start': '2018-06-16', 'end': '2018-06-17', 'tags': [], 'website_url': 'http://example.org/2018-06-16-AMY-event', 'venue': '', 'address': '', 'country': '', 'latitude': None, 'longitude': None, } }, { 'badge': 'dc-instructor', 'awarded': '2018-06-16', 'event': None, }, ] self.assertEqual(data['awards'], expected['awards']) # test expected Tasks output expected['tasks'] = [ { 'event': { 'slug': '2018-06-16-AMY-event', 'start': '2018-06-16', 'end': '2018-06-17', 'tags': [], 'website_url': 'http://example.org/2018-06-16-AMY-event', 'venue': '', 'address': '', 'country': '', 'latitude': None, 'longitude': None, }, 'role': 'instructor', }, ] self.assertEqual(data['tasks'], expected['tasks']) # test expected Languages output expected['languages'] = [ 'English', 'French', ] self.assertEqual(data['languages'], expected['languages']) # test expected TrainingRequests output expected['training_requests'] = [{ # these are generated by Django, so we borrow them from the # output 'created_at': data['training_requests'][0]['created_at'], 'last_updated_at': data['training_requests'][0]['last_updated_at'], 'state': 'Pending', 'group_name': 'Mosquitos', 'personal': 'User', 'middle': '', 'family': 'Primary', 'email': '*****@*****.**', 'github': 'primary_user', 'occupation': 'undisclosed', 'occupation_other': '', 'affiliation': 'AMY', 'location': 'Worldwide', 'country': 'W3', 'underresourced': False, 'domains': ['Chemistry'], 'domains_other': 'E-commerce', 'underrepresented': '', 'nonprofit_teaching_experience': 'Voluntary teacher', 'previous_involvement': ['instructor'], 'previous_training': 'A certification or short course', 'previous_training_other': '', 'previous_training_explanation': 'A course for voluntary teaching', 'previous_experience': 'Teaching assistant for a full course', 'previous_experience_other': '', 'previous_experience_explanation': 'After the course I became a TA', 'programming_language_usage_frequency': 'A few times a week', 'teaching_frequency_expectation': 'Several times a year', 'teaching_frequency_expectation_other': '', 'max_travelling_frequency': 'Not at all', 'max_travelling_frequency_other': '', 'reason': 'I want to became an instructor', 'comment': 'I like trains', 'training_completion_agreement': True, 'workshop_teaching_agreement': True, 'data_privacy_agreement': True, 'code_of_conduct_agreement': True, }] self.assertEqual(len(data['training_requests']), 1) self.assertEqual(data['training_requests'][0], expected['training_requests'][0]) # test expected TrainingProgress output expected['training_progresses'] = [ { # these are generated by Django, so we borrow them from the # output 'created_at': data['training_progresses'][0]['created_at'], 'last_updated_at': data['training_progresses'][0]['last_updated_at'], 'requirement': { 'name': 'Discussion', 'url_required': False, 'event_required': False, }, 'state': 'Passed', 'discarded': False, 'evaluated_by': None, 'event': { 'slug': '2018-06-16-AMY-event', 'start': '2018-06-16', 'end': '2018-06-17', 'tags': [], 'website_url': 'http://example.org/2018-06-16-AMY-event', 'venue': '', 'address': '', 'country': '', 'latitude': None, 'longitude': None, }, 'url': None, }, { # these are generated by Django, so we borrow them from the # output 'created_at': data['training_progresses'][1]['created_at'], 'last_updated_at': data['training_progresses'][1]['last_updated_at'], 'requirement': { 'name': 'DC Homework', 'url_required': True, 'event_required': False, }, 'state': 'Failed', 'discarded': False, 'evaluated_by': { 'name': 'Super User', }, 'event': None, 'url': 'http://example.org/homework', }, ] self.assertEqual(len(data['training_progresses']), 2) self.assertEqual(data['training_progresses'][0], expected['training_progresses'][0]) self.assertEqual(data['training_progresses'][1], expected['training_progresses'][1])
def profileupdaterequest_accept(request, request_id, person_id=None): """ Accept the profile update by rewriting values to selected user's profile. IMPORTANT: we do not rewrite all of the data users input (like other gender, or other lessons). All of it is still in the database model ProfileUpdateRequest, but does not get written to the Person model object. """ profileupdate = get_object_or_404(ProfileUpdateRequest, active=True, pk=request_id) airport = get_object_or_404(Airport, iata__iexact=profileupdate.airport_iata) if person_id is None: person = Person() # since required perms change depending on `person_id`, we have to # check the perms programmatically; here user is required # `workshops.add_person` in order to add a new person if not request.user.has_perm('workshops.add_person'): raise PermissionDenied else: person = get_object_or_404(Person, pk=person_id) person_name = str(person) # since required perms change depending on `person_id`, we have to # check the perms programmatically; here user is required # `workshops.change_person` in order to set existing person's fields if not request.user.has_perm('workshops.change_person'): raise PermissionDenied person.personal = profileupdate.personal person.middle = profileupdate.middle person.family = profileupdate.family person.email = profileupdate.email person.affiliation = profileupdate.affiliation person.country = profileupdate.country person.airport = airport person.github = profileupdate.github person.twitter = profileupdate.twitter person.url = profileupdate.website # if occupation is "Other", simply save the `occupation_other` field, # otherwise get full display of occupation (since it's a choice field) if profileupdate.occupation == '': person.occupation = profileupdate.occupation_other else: person.occupation = profileupdate.get_occupation_display() person.orcid = profileupdate.orcid person.gender = profileupdate.gender person.user_notes = profileupdate.notes with transaction.atomic(): # we need person to exist in the database in order to set domains and # lessons if not person.id: try: person.username = create_username(person.personal, person.family) person.save() except IntegrityError: messages.error( request, 'Cannot update profile: some database constraints weren\'t' ' fulfilled. Make sure that user name, GitHub user name,' ' Twitter user name, or email address are unique.' ) return redirect(profileupdate.get_absolute_url()) person.domains.set(list(profileupdate.domains.all())) person.languages.set(profileupdate.languages.all()) try: person.save() except IntegrityError: messages.error( request, 'Cannot update profile: some database constraints weren\'t' 'fulfilled. Make sure that user name, GitHub user name,' 'Twitter user name, or email address are unique.' ) return redirect(profileupdate.get_absolute_url()) # Since Person.lessons uses a intermediate model Qualification, we # ought to operate on Qualification objects instead of using # Person.lessons as a list. # erase old lessons Qualification.objects.filter(person=person).delete() # add new Qualification.objects.bulk_create([ Qualification(person=person, lesson=L) for L in profileupdate.lessons.all() ]) profileupdate.active = False profileupdate.save() if person_id is None: messages.success(request, 'New person was added successfully.') else: messages.success(request, '{} was updated successfully.'.format(person_name)) return redirect(person.get_absolute_url())
def person_bulk_add_confirmation(request): """ This view allows for manipulating and saving session-stored upload data. """ persons_tasks = request.session.get('bulk-add-people') # if the session is empty, add message and redirect if not persons_tasks: messages.warning( request, "Could not locate CSV data, please try the upload again.") return redirect('person_bulk_add') if request.method == 'POST': # update values if user wants to change them personals = request.POST.getlist("personal") middles = request.POST.getlist("middle") families = request.POST.getlist("family") emails = request.POST.getlist("email") events = request.POST.getlist("event") roles = request.POST.getlist("role") data_update = zip(personals, middles, families, emails, events, roles) for k, record in enumerate(data_update): personal, middle, family, email, event, role = record persons_tasks[k]['person'] = { 'personal': personal, 'middle': middle, 'family': family, 'email': email } # when user wants to drop related event they will send empty string # so we should unconditionally accept new value for event even if # it's an empty string persons_tasks[k]['event'] = event persons_tasks[k]['role'] = role persons_tasks[k]['errors'] = None # reset here # save updated data to the session request.session['bulk-add-people'] = persons_tasks # check if user wants to verify or save, or cancel if request.POST.get('verify', None): # if there's "verify" in POST, then do only verification any_errors = verify_upload_person_task(persons_tasks) if any_errors: messages.add_message( request, messages.ERROR, "Please make sure to fix all errors " "listed below.") context = { 'title': 'Confirm uploaded data', 'persons_tasks': persons_tasks } return render(request, 'workshops/person_bulk_add_results.html', context) elif (request.POST.get('confirm', None) and not request.POST.get('cancel', None)): # there must be "confirm" and no "cancel" in POST in order to save try: records = 0 with transaction.atomic(): for row in persons_tasks: # create person p = Person(**row['person']) p.save() records += 1 # create task if data supplied if row['event'] and row['role']: e = Event.objects.get(slug=row['event']) r = Role.objects.get(name=row['role']) t = Task(person=p, event=e, role=r) t.save() records += 1 except (IntegrityError, ObjectDoesNotExist) as e: messages.add_message( request, messages.ERROR, "Error saving data to the database: {}. " "Please make sure to fix all errors " "listed below.".format(e)) verify_upload_person_task(persons_tasks) context = { 'title': 'Confirm uploaded data', 'persons_tasks': persons_tasks } return render(request, 'workshops/person_bulk_add_results.html', context) else: request.session['bulk-add-people'] = None messages.add_message( request, messages.SUCCESS, "Successfully bulk-loaded {} records.".format(records)) return redirect('person_bulk_add') else: # any "cancel" or no "confirm" in POST cancels the upload request.session['bulk-add-people'] = None return redirect('person_bulk_add') else: # alters persons_tasks via reference verify_upload_person_task(persons_tasks) context = { 'title': 'Confirm uploaded data', 'persons_tasks': persons_tasks } return render(request, 'workshops/person_bulk_add_results.html', context)
class TestExportingPersonData(BaseExportingTest): def setUp(self): # don't remove all badges # super().setUp() # prepare user self.user = Person( username="******", personal="User", family="Primary", email="*****@*****.**", is_active=True, data_privacy_agreement=True, ) self.user.set_password('password') self.user.save() # save API endpoint URL self.url = reverse('api:export-person-data') def login(self): """Overwrite BaseExportingTest's login method: instead of loggin in as an admin, use a normal user.""" self.client.login(username='******', password='******') def prepare_data(self, user): """Populate relational fields for the user.""" # create and set airport for the user airport = Airport.objects.create( iata='DDD', fullname='Airport 55x105', country='CM', latitude=55.0, longitude=105.0, ) self.user.airport = airport self.user.save() # create a fake organization test_host = Organization.objects.create( domain='example.com', fullname='Test Organization') # create an event that will later be used event = Event.objects.create( start=datetime.date(2018, 6, 16), end=datetime.date(2018, 6, 17), slug='2018-06-16-AMY-event', host=test_host, url='http://example.org/2018-06-16-AMY-event', ) # add a role Role.objects.create(name='instructor', verbose_name='Instructor') # add an admin user self.setup_admin() # award user some badges via awards (intermediary model) # one badge was awarded for the event award1 = Award.objects.create( person=self.user, badge=Badge.objects.get(name='swc-instructor'), event=event, awarded=datetime.date(2018, 6, 16), ) # second badge was awarded without any connected event award2 = Award.objects.create( person=self.user, badge=Badge.objects.get(name='dc-instructor'), awarded=datetime.date(2018, 6, 16), ) # user took part in the event as an instructor self.user.task_set.create( event=event, role=Role.objects.get(name='instructor'), ) # user knows a couple of languages self.user.languages.set( Language.objects.filter(name__in=['English', 'French']) ) # add training requests training_request = TrainingRequest.objects.create( # mixins data_privacy_agreement=True, code_of_conduct_agreement=True, state='p', # pending person=self.user, group_name='Mosquitos', personal='User', middle='', family='Primary', email='*****@*****.**', github='primary_user', occupation='undisclosed', occupation_other='', affiliation='AMY', location='Worldwide', country='W3', underresourced=False, # need to set it below # domains=KnowledgeDomain.objects.first(), domains_other='E-commerce', underrepresented='yes', underrepresented_details='LGBTQ', nonprofit_teaching_experience='Voluntary teacher', # need to set it below # previous_involvement=Role.objects.filter(name='instructor'), previous_training='course', previous_training_other='', previous_training_explanation='A course for voluntary teaching', previous_experience='ta', previous_experience_other='', previous_experience_explanation='After the course I became a TA', programming_language_usage_frequency='weekly', teaching_frequency_expectation='monthly', max_travelling_frequency='not-at-all', max_travelling_frequency_other='', reason='I want to became an instructor', user_notes='I like trains', training_completion_agreement=True, workshop_teaching_agreement=True, ) training_request.domains.set([KnowledgeDomain.objects.first()]) training_request.previous_involvement.set( Role.objects.filter(name='instructor')) # add some training progress TrainingProgress.objects.create( trainee=self.user, requirement=TrainingRequirement.objects.get(name='Discussion'), state='p', # passed event=event, evaluated_by=None, discarded=False, url=None, ) TrainingProgress.objects.create( trainee=self.user, requirement=TrainingRequirement.objects.get(name='DC Homework'), state='f', # failed event=None, evaluated_by=self.admin, discarded=False, url='http://example.org/homework', ) def test_unauthorized_access(self): """Make sure only authenticated users can access.""" # logout self.client.logout() # retrieve endpoint rv = self.client.get(self.url) # make sure it's inaccessible self.assertEqual(rv.status_code, 401) def test_only_for_one_user(self): """Make sure the results are available only for the logged-in user, no-one else.""" # prepare a different user self.second_user = Person( username="******", personal="User", family="Secondary", email="*****@*****.**", is_active=True, data_privacy_agreement=True, ) self.second_user.set_password('password') self.second_user.save() # login as first user self.client.login(username='******', password='******') # retrieve endpoint rv = self.client.get(self.url) self.assertEqual(rv.status_code, 200) # make sure this endpoint returns current user data self.assertEqual(rv.json()['username'], 'primary_user') # login as second user self.client.login(username='******', password='******') rv = self.client.get(self.url) self.assertEqual(rv.status_code, 200) # make sure this endpoint does not return first user data now self.assertEqual(rv.json()['username'], 'secondary_user') def test_all_related_objects_shown(self): """Test if all related fields are present in data output.""" self.login() # retrieve endpoint rv = self.client.get(self.url) self.assertEqual(rv.status_code, 200) # API results parsed as JSON user_data = rv.json() user_data_keys = user_data.keys() # make sure these fields are NOT in the API output missing_fields = [ 'password', 'is_active', ] # simple (non-relational) fields expected in API output expected_fields = [ 'data_privacy_agreement', 'personal', 'middle', 'family', 'email', 'username', 'gender', 'may_contact', 'publish_profile', 'github', 'twitter', 'url', 'user_notes', 'affiliation', 'occupation', 'orcid', ] # relational fields expected in API output expected_relational = [ 'airport', 'badges', 'lessons', 'domains', 'languages', 'tasks', # renamed in serializer (was: task_set) 'awards', # renamed in serializer (was: award_set) 'training_requests', # renamed from "trainingrequest_set" 'training_progresses', # renamed from "trainingprogress_set" ] # ensure missing fields are not to be found in API output for field in missing_fields: self.assertNotIn(field, user_data_keys) # ensure required fields are present for field in expected_fields + expected_relational: self.assertIn(field, user_data_keys) def test_relational_fields_structure(self): """Make sure relational fields available via API endpoints retain a specific structure.""" self.prepare_data(user=self.user) self.login() # retrieve endpoint rv = self.client.get(self.url) self.assertEqual(rv.status_code, 200) # API results parsed as JSON data = rv.json() # expected data dict expected = dict() # test expected Airport output expected['airport'] = { 'iata': 'DDD', 'fullname': 'Airport 55x105', 'country': 'CM', 'latitude': 55.0, 'longitude': 105.0, } self.assertEqual(data['airport'], expected['airport']) # test expected Badges output expected['badges'] = [ { 'name': 'swc-instructor', 'title': 'Software Carpentry Instructor', 'criteria': 'Teaching at Software Carpentry workshops or' ' online', }, { 'name': 'dc-instructor', 'title': 'Data Carpentry Instructor', 'criteria': 'Teaching at Data Carpentry workshops or' ' online', }, ] self.assertEqual(data['badges'], expected['badges']) # test expected Awards output expected['awards'] = [ { 'badge': 'swc-instructor', 'awarded': '2018-06-16', 'event': { 'slug': '2018-06-16-AMY-event', 'start': '2018-06-16', 'end': '2018-06-17', 'tags': [], 'website_url': 'http://example.org/2018-06-16-AMY-event', 'venue': '', 'address': '', 'country': '', 'latitude': None, 'longitude': None, } }, { 'badge': 'dc-instructor', 'awarded': '2018-06-16', 'event': None, }, ] self.assertEqual(data['awards'], expected['awards']) # test expected Tasks output expected['tasks'] = [ { 'event': { 'slug': '2018-06-16-AMY-event', 'start': '2018-06-16', 'end': '2018-06-17', 'tags': [], 'website_url': 'http://example.org/2018-06-16-AMY-event', 'venue': '', 'address': '', 'country': '', 'latitude': None, 'longitude': None, }, 'role': 'instructor', }, ] self.assertEqual(data['tasks'], expected['tasks']) # test expected Languages output expected['languages'] = [ 'English', 'French', ] self.assertEqual(data['languages'], expected['languages']) # test expected TrainingRequests output expected['training_requests'] = [ { # these are generated by Django, so we borrow them from the # output 'created_at': data['training_requests'][0]['created_at'], 'last_updated_at': data['training_requests'][0]['last_updated_at'], 'state': 'Pending', 'group_name': 'Mosquitos', 'personal': 'User', 'middle': '', 'family': 'Primary', 'email': '*****@*****.**', 'github': 'primary_user', 'occupation': 'undisclosed', 'occupation_other': '', 'affiliation': 'AMY', 'location': 'Worldwide', 'country': 'W3', 'underresourced': False, 'domains': ['Chemistry'], 'domains_other': 'E-commerce', 'underrepresented': 'yes', 'underrepresented_details': 'LGBTQ', 'nonprofit_teaching_experience': 'Voluntary teacher', 'previous_involvement': ['instructor'], 'previous_training': 'A certification or short course', 'previous_training_other': '', 'previous_training_explanation': 'A course for voluntary teaching', 'previous_experience': 'Teaching assistant for a full course', 'previous_experience_other': '', 'previous_experience_explanation': 'After the course I became a TA', 'programming_language_usage_frequency': 'A few times a week', 'teaching_frequency_expectation': 'Several times a year', 'teaching_frequency_expectation_other': '', 'max_travelling_frequency': 'Not at all', 'max_travelling_frequency_other': '', 'reason': 'I want to became an instructor', 'user_notes': 'I like trains', 'training_completion_agreement': True, 'workshop_teaching_agreement': True, 'data_privacy_agreement': True, 'code_of_conduct_agreement': True, } ] self.assertEqual(len(data['training_requests']), 1) self.assertEqual(data['training_requests'][0], expected['training_requests'][0]) # test expected TrainingProgress output expected['training_progresses'] = [ { # these are generated by Django, so we borrow them from the # output 'created_at': data['training_progresses'][0]['created_at'], 'last_updated_at': data['training_progresses'][0]['last_updated_at'], 'requirement': { 'name': 'Discussion', 'url_required': False, 'event_required': False, }, 'state': 'Passed', 'discarded': False, 'evaluated_by': None, 'event': { 'slug': '2018-06-16-AMY-event', 'start': '2018-06-16', 'end': '2018-06-17', 'tags': [], 'website_url': 'http://example.org/2018-06-16-AMY-event', 'venue': '', 'address': '', 'country': '', 'latitude': None, 'longitude': None, }, 'url': None, }, { # these are generated by Django, so we borrow them from the # output 'created_at': data['training_progresses'][1]['created_at'], 'last_updated_at': data['training_progresses'][1]['last_updated_at'], 'requirement': { 'name': 'DC Homework', 'url_required': True, 'event_required': False, }, 'state': 'Failed', 'discarded': False, 'evaluated_by': { 'name': 'Super User', }, 'event': None, 'url': 'http://example.org/homework', }, ] self.assertEqual(len(data['training_progresses']), 2) self.assertEqual(data['training_progresses'][0], expected['training_progresses'][0]) self.assertEqual(data['training_progresses'][1], expected['training_progresses'][1])