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 testCheckForNonContactablePerson(self): """Make sure `may_contact` doesn't impede `check()`.""" # totally fake Task, Role and Event data LC_org = Organization.objects.get(domain="librarycarpentry.org") e = Event.objects.create( slug="test-event", host=Organization.objects.first(), administrator=LC_org, start=date.today() + timedelta(days=7), end=date.today() + timedelta(days=8), country="GB", venue="Ministry of Magic", address="Underground", latitude=20.0, longitude=20.0, url="https://test-event.example.com", ) e.tags.set( Tag.objects.filter( name__in=["SWC", "DC", "LC", "automated-email"])) p = Person(personal="Harry", family="Potter", email="*****@*****.**", may_contact=True) # contact allowed r = Role(name="supporting-instructor") t = Task(event=e, person=p, role=r) self.assertEqual(NewSupportingInstructorAction.check(t), True) p.may_contact = False # contact disallowed self.assertEqual(NewSupportingInstructorAction.check(t), True)
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 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 testCheckConditions(self): """Make sure `check` works for various input data.""" # totally fake Task, Role and Event data LC_org = Organization.objects.get(domain="librarycarpentry.org") e = Event.objects.create( slug="test-event", host=Organization.objects.first(), administrator=LC_org, start=date.today() + timedelta(days=7), end=date.today() + timedelta(days=8), ) e.tags.set( Tag.objects.filter( name__in=["SWC", "DC", "LC", "automated-email"])) p = Person(personal="Harry", family="Potter", email="*****@*****.**") r = Role(name="supporting-instructor") t = Task(event=e, person=p, role=r) # 1st case: everything is good self.assertEqual(NewSupportingInstructorAction.check(t), True) # 2nd case: event has no start date, but still valid tags e.start = None e.save() self.assertEqual(NewSupportingInstructorAction.check(t), True) # 3rd case: event start date in past, but still valid tags e.start = date(2000, 1, 1) e.save() self.assertEqual(NewSupportingInstructorAction.check(t), False) # bring back the good date e.start = date.today() + timedelta(days=7) e.save() self.assertEqual(NewSupportingInstructorAction.check(t), True) # 4th case: event is tagged with one (or more) excluding tags e.tags.add(Tag.objects.get(name="cancelled")) self.assertEqual(NewSupportingInstructorAction.check(t), False) e.tags.remove(Tag.objects.get(name="cancelled")) # 5th case: role is different than 'supporting-instructor' r.name = "helper" self.assertEqual(NewSupportingInstructorAction.check(t), False) r.name = "supporting-instructor" # 6th case: no administrator e.administrator = None e.save() self.assertEqual(NewSupportingInstructorAction.check(t), False) e.administrator = LC_org # 7th case: wrong administrator (self organized or instructor training) e.administrator = Organization.objects.get(domain="self-organized") e.save() self.assertEqual(NewSupportingInstructorAction.check(t), False) e.administrator = Organization.objects.get(domain="carpentries.org") e.save() self.assertEqual(NewSupportingInstructorAction.check(t), False)
def parse(self, speaker): speaker['name'] = speaker['name'].strip() personal = speaker['name'].rsplit(' ', 1)[0] family = speaker['name'].rsplit(' ', 1)[-1] return Person( username=speaker['username'], personal=personal, family=family, email=speaker['email'], url=speaker['absolute_url'], )
def _upload_person_task_csv(request, uploaded_file): persons_tasks = [] reader = csv.DictReader(uploaded_file) for row in reader: person_fields = dict((col, row[col].strip()) for col in PERSON_UPLOAD_FIELDS) person = Person(**person_fields) entry = {'person': person, 'task' : None} if row.get('event', None) and row.get('role', None): try: event = Event.objects.get(slug=row['event']) role = Role.objects.get(name=row['role']) entry['task'] = Task(person=person, event=event, role=role) import sys except Event.DoesNotExist: messages.add_message(request, messages.ERROR, \ 'Event with slug {} does not exist.'.format(row['event'])) except Role.DoesNotExist: messages.add_message(request, messages.ERROR, \ 'Role with name {} does not exist.'.format(row['role'])) except Role.MultipleObjectsReturned: messages.add_message(request, messages.ERROR, \ 'More than one role named {} exists.'.format(row['role'])) persons_tasks.append(entry) return persons_tasks
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 verify_upload_person_task(data, match=False): """ Verify that uploaded data is correct. Show errors by populating `errors` dictionary item. This function changes `data` in place. If `match` provided, it will try to match with first similar person. """ errors_occur = False for item in data: errors = [] info = [] event = item.get("event", None) existing_event = None if event: try: existing_event = Event.objects.get(slug=event) except Event.DoesNotExist: errors.append( 'Event with slug "{0}" does not exist.'.format(event)) except Event.MultipleObjectsReturned: errors.append( 'More than one event named "{0}" exists.'.format(event)) role = item.get("role", None) existing_role = None if role: try: existing_role = Role.objects.get(name=role) except Role.DoesNotExist: errors.append( 'Role with name "{0}" does not exist.'.format(role)) except Role.MultipleObjectsReturned: errors.append( 'More than one role named "{0}" exists.'.format(role)) # check if the user exists, and if so: check if existing user's # personal and family names are the same as uploaded email = item.get("email", "") personal = item.get("personal", "") family = item.get("family", "") person_id = item.get("existing_person_id", None) person = None # try to match with first similar person if match is True: try: person = Person.objects.get(email=email) except (Person.DoesNotExist, Person.MultipleObjectsReturned): person = None else: info.append("Existing record for person will be used.") person_id = person.pk elif person_id: try: person = Person.objects.get(id=int(person_id)) except (ValueError, TypeError, Person.DoesNotExist): person = None info.append("Could not match selected person. New record will " "be created.") else: info.append("Existing record for person will be used.") elif not person_id: try: Person.objects.get(email=email) except (Person.DoesNotExist, Person.MultipleObjectsReturned): pass else: errors.append("Person with this email address already exists.") try: if item.get("username"): Person.objects.get(username=item.get("username")) except Person.DoesNotExist: pass else: errors.append("Person with this username already exists.") if not email and not person: info.append("It's highly recommended to add an email address.") if person: # force details from existing record item["personal"] = personal = person.personal item["family"] = family = person.family item["email"] = email = person.email item["username"] = person.username item["existing_person_id"] = person_id item["person_exists"] = True else: # force a newly created username if not item.get("username"): item["username"] = create_username(personal, family) item["person_exists"] = False info.append("Person and task will be created.") # let's check if there's someone else named this way similar_persons = Person.objects.filter( Q(personal=personal, family=family) | Q(email=email) & ~Q(email="") & Q(email__isnull=False)) # need to cast to list, otherwise it won't JSON-ify item["similar_persons"] = list( zip( similar_persons.values_list("id", flat=True), map(lambda x: str(x), similar_persons), )) if existing_event and person and existing_role: # person, their role and a corresponding event exist, so # let's check if the task exists try: Task.objects.get(event=existing_event, person=person, role=existing_role) except Task.DoesNotExist: info.append("Task will be created.") else: info.append("Task already exists.") # let's check what Person model validators want to say try: p = Person(personal=personal, family=family, email=email, username=item["username"]) p.clean_fields(exclude=["password"]) except ValidationError as e: for k, v in e.message_dict.items(): errors.append("{}: {}".format(k, v)) if not role: errors.append("Must have a role.") if not event: errors.append("Must have an event.") item["errors"] = errors if errors: errors_occur = True item["info"] = info return errors_occur
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
def verify_upload_person_task(data): """ Verify that uploaded data is correct. Show errors by populating ``errors`` dictionary item. This function changes ``data`` in place. """ errors_occur = False for item in data: errors = [] info = [] event = item.get('event', None) existing_event = None if event: try: existing_event = Event.objects.get(slug=event) except Event.DoesNotExist: errors.append('Event with slug {0} does not exist.' .format(event)) role = item.get('role', None) existing_role = None if role: try: existing_role = Role.objects.get(name=role) except Role.DoesNotExist: errors.append('Role with name {0} does not exist.' .format(role)) except Role.MultipleObjectsReturned: errors.append('More than one role named {0} exists.' .format(role)) # check if the user exists, and if so: check if existing user's # personal and family names are the same as uploaded email = item.get('email', None) personal = item.get('personal', None) family = item.get('family', None) person_id = item.get('existing_person_id', None) person = None if person_id: try: person = Person.objects.get(id=int(person_id)) except (ValueError, TypeError, Person.DoesNotExist): person = None info.append('Could not match selected person. New record will ' 'be created.') else: info.append('Existing record for person will be used.') if not email and not person: info.append('It\'s highly recommended to add an email address.') if person: # force details from existing record item['personal'] = personal = person.personal item['family'] = family = person.family item['email'] = email = person.email item['username'] = person.username item['person_exists'] = True else: # force a newly created username if not item.get('username'): item['username'] = create_username(personal, family) item['person_exists'] = False info.append('Person and task will be created.') # let's check if there's someone else named this way similar_persons = Person.objects.filter( Q(personal=personal, family=family) | Q(email=email) & ~Q(email='') & Q(email__isnull=False) ) # need to cast to list, otherwise it won't JSON-ify item['similar_persons'] = list(similar_persons.values( 'id', 'personal', 'middle', 'family', 'email', 'username', )) if existing_event and person and existing_role: # person, their role and a corresponding event exist, so # let's check if the task exists try: Task.objects.get(event=existing_event, person=person, role=existing_role) except Task.DoesNotExist: info.append('Task will be created.') else: info.append('Task already exists.') # let's check what Person model validators want to say try: p = Person(personal=personal, family=family, email=email, username=item['username']) p.clean_fields(exclude=['password']) except ValidationError as e: for k, v in e.message_dict.items(): errors.append('{}: {}'.format(k, v)) if not role: errors.append('Must have a role.') if not event: errors.append('Must have an event.') if errors: errors_occur = True item['errors'] = errors if info: item['info'] = info return errors_occur
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 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)
def testCheckConditions(self): """Make sure `check` works for various input data.""" # totally fake Task, Role and Event data LC_org = Organization.objects.get(domain="librarycarpentry.org") e = Event.objects.create( slug="test-event", host=Organization.objects.first(), administrator=LC_org, start=date.today() + timedelta(days=7), end=date.today() + timedelta(days=8), # 2019-12-24: we no longer require published conditions met for # the event, so the values below were commented out # country='GB', # venue='Ministry of Magic', # address='Underground', # latitude=20.0, # longitude=20.0, # url='https://test-event.example.com', ) e.tags.set(Tag.objects.filter(name__in=["SWC", "DC", "LC", "automated-email"])) p = Person(personal="Harry", family="Potter", email="*****@*****.**") r = Role(name="instructor") t = Task(event=e, person=p, role=r) # 1st case: everything is good self.assertEqual(NewInstructorAction.check(t), True) # 2nd case: event has no start date, but still valid tags e.start = None e.save() self.assertEqual(NewInstructorAction.check(t), True) # 3rd case: event start date in past, but still valid tags e.start = date(2000, 1, 1) e.save() self.assertEqual(NewInstructorAction.check(t), False) # bring back the good date e.start = date.today() + timedelta(days=7) e.save() self.assertEqual(NewInstructorAction.check(t), True) # 4th case: event is tagged with one (or more) excluding tags e.tags.add(Tag.objects.get(name="cancelled")) self.assertEqual(NewInstructorAction.check(t), False) e.tags.remove(Tag.objects.get(name="cancelled")) # 5th case: role is different than 'instructor' r.name = "helper" self.assertEqual(NewInstructorAction.check(t), False) r.name = "instructor" # 6th case: no administrator e.administrator = None e.save() self.assertEqual(NewInstructorAction.check(t), False) e.administrator = LC_org # 7th case: wrong administrator (self organized or instructor training) e.administrator = Organization.objects.get(domain="self-organized") e.save() self.assertEqual(NewInstructorAction.check(t), False) e.administrator = Organization.objects.get(domain="carpentries.org") e.save() self.assertEqual(NewInstructorAction.check(t), False)
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])
def verify_upload_person_task(data): """ Verify that uploaded data is correct. Show errors by populating ``errors`` dictionary item. This function changes ``data`` in place. """ errors_occur = False for item in data: errors = [] info = [] event = item.get('event', None) existing_event = None if event: try: existing_event = Event.objects.get(slug=event) except Event.DoesNotExist: errors.append('Event with slug {0} does not exist.' .format(event)) role = item.get('role', None) existing_role = None if role: try: existing_role = Role.objects.get(name=role) except Role.DoesNotExist: errors.append('Role with name {0} does not exist.' .format(role)) except Role.MultipleObjectsReturned: errors.append('More than one role named {0} exists.' .format(role)) # check if the user exists, and if so: check if existing user's # personal and family names are the same as uploaded email = item.get('email', None) personal = item.get('personal', None) family = item.get('family', None) person = None if email: try: # check if first and last name matches person in the database person = Person.objects.get(email__iexact=email) for which, actual, uploaded in ( ('personal', person.personal, personal), ('family', person.family, family) ): if (actual == uploaded) or (not actual and not uploaded): pass else: errors.append('{0} mismatch: database "{1}" ' 'vs uploaded "{2}".' .format(which, actual, uploaded)) except Person.DoesNotExist: # in this case we need to add a new person pass else: if existing_event and person and existing_role: # person, their role and a corresponding event exist, so # let's check if the task exists try: Task.objects.get(event=existing_event, person=person, role=existing_role) except Task.DoesNotExist: info.append('Task will be created.') else: info.append('Task already exists.') else: info.append('It\'s highly recommended to add an email address.') if person: # force username from existing record item['username'] = person.username item['person_exists'] = True else: # force a newly created username if not item.get('username'): item['username'] = create_username(personal, family) item['person_exists'] = False info.append('Person and task will be created.') try: # let's check if there's someone else named this way similar_person = Person.objects.get(personal=personal, family=family) except Person.DoesNotExist: pass except Person.MultipleObjectsReturned: persons = [ str(person) for person in Person.objects.filter(personal=personal, family=family) ] info.append('There\'s a couple of matching persons in the ' 'database: {}. ' 'Use email to merge.'.format(', '.join(persons))) else: info.append('There\'s a matching person in the database: {}. ' 'Use their email to merge.'.format(similar_person)) # let's check what Person model validators want to say try: p = Person(personal=personal, family=family, email=email, username=item['username']) p.clean_fields(exclude=['password']) except ValidationError as e: for k, v in e.message_dict.items(): errors.append('{}: {}'.format(k, v)) if not role: errors.append('Must have a role.') if not event: errors.append('Must have an event.') if errors: errors_occur = True item['errors'] = errors if info: item['info'] = info return errors_occur
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())