def recursive_update(self, kind, id, params, preview=False): """Brute-force changes to an entity and all its children. Intentionally EXCLUDES pd entities in this recursion, because there could be thousands of entities to move and this could not be done without a timeout. Creates logs of its activity in case anything needs to be undone. Logs are JSON serialization of the set of all entities before changes and the set of entities after changes. This way, if some data is erased by this function, it can be found again. Returns a list of changed entities. Lana. Lana. LAAANAAAA. Danger zone. """ if self.user.user_type != 'god': raise PermissionDenied() # Get the requested entity's children, limiting ourselves to # non-deleted ones. Keeping deleted entities around is really only for # emergency data recovery. entities = self._get_children(kind, id, [('deleted =', False)], exclude_kinds=['pd']) # keep a record of these entities before they were changed before_snapshot = [e.to_dict() for e in entities] # Make all the requested property changes to all the retrieved # entities, if those properties exist. It's important to have this # flexibility because a single conceptual change (e.g. changing cohort # associations of all children) requires various kinds of property # updates (e.g. to assc_cohort_list of Activity and cohort of Pd). # Also build a unique set of only the changed entities to make the # db.put() as efficient as possible. to_put = set() for e in entities: for k, v in params.items(): if hasattr(e, k) and getattr(e, k) != v: to_put.add(e) setattr(e, k, v) to_put = list(to_put) after_snapshot = [e.to_dict() for e in to_put] if not preview: db.put(list(to_put)) # save the log body = json.dumps({ 'entities before recursive update': before_snapshot, 'entities after recursive update': after_snapshot, }) log_entry = LogEntry.create(log_name='recursive_udpate', body=body) log_entry.put() return to_put
def delete_everything(self): if self.user.user_type == 'god' and util.is_development(): util.delete_everything() else: raise PermissionDenied("Only gods working on a development server " "can delete everything.") return True
def import_links(self, program, session_ordinal, filename): """Read in a cloud storage file full of Qualtrics unique links. See https://docs.google.com/document/d/1xrTaGf8-f0wyXg5ZnIH1O6uzSv-ro_ei1MpZOlwCXjA/edit """ if self.user.user_type != 'god': raise PermissionDenied() # Our convention is to read link csv files out of a bucket named by # the app id. But for silly google reasons, the app id is prefixed by # 's~' internally (something to do with text search indexing). Take off # that bit before using the app id. app_id = app_identity.get_application_id() if app_id[:2] == 's~': app_id = app_id[2:] # Set GCS path dependent on program and session. path = '/{}_unique_qualtrics_links/{}-{}/{}'.format( app_id, program.abbreviation, session_ordinal, filename) retry_params = gcs.RetryParams() links = [] # Try the gcs transaction try: f = gcs.open(path, mode='r', retry_params=retry_params) except gcs.NotFoundError: return ('GCS File not found. Did you upload a new file to the ' 'bucket?') # Try the csv read try: reader = csv.reader(f) for row in reader: if row[7] == 'Link': continue link = row[7] l = QualtricsLink.create(key_name=link, link=link, program=program.id, session_ordinal=session_ordinal) links.append(l) except Exception as e: logging.error( 'Something went wrong with the CSV import! {}'.format(e)) logging.error('CSV has been deleted, try uploading again.') # Throwing out links links = [] finally: f.close() gcs.delete(path) db.put(links) return len(links)
def impersonate(self, target): """Set a special user id in the session so get_current_user() returns that user. Makes the website look like the impersonate user would see it, while the original user remains logged in. Raises PermissionDenied.""" normal_user = self.get_current_user(method='normal') if normal_user.can_impersonate(target): # set the impersonated user self.session['impersonated_user'] = target.id else: raise PermissionDenied( "Not allowed to impersonate {}".format(target.id))
def update(self, kind, id, kwargs): entity = core.Model.get_from_path(kind, id) if not self.user.has_permission('put', entity): raise PermissionDenied() # if creating a user, can this user create this TYPE of user # this is necessary to check if user can promote target user # to the proposed level; that does not get checked in user.can_put() if kind == 'user' and 'user_type' in kwargs: if not self.user.can_put_user_type(kwargs['user_type']): raise PermissionDenied( "{} cannot create users of type {}.".format( self.user.user_type, kwargs['user_type'])) # some updates require additional validity checks if kind in config.kinds_requiring_put_validation: kwargs = entity.validate_put(kwargs) # run the actual update for k, v in kwargs.items(): setattr(entity, k, v) entity.put() return entity
def associate(self, action, from_entity, to_entity, put=True): logging.info( 'Api.associate(action={}, from_entity={}, to_entity={}, put={})'. format(action, from_entity.id, to_entity.id, put)) # create the requested association from_kind = core.get_kind(from_entity) to_kind = core.get_kind(to_entity) if not self.user.can_associate(action, from_entity, to_entity): raise PermissionDenied("association failure!") if action == 'set_owner': property_name = 'owned_' + to_kind + '_list' elif action == 'associate': property_name = 'assc_' + to_kind + '_list' relationship_list = getattr(from_entity, property_name) if to_entity.id not in relationship_list: relationship_list.append(to_entity.id) setattr(from_entity, property_name, relationship_list) # recurse through cascading relationships start_cascade = (action in ['associate', 'set_owner'] and from_kind == 'user' and to_kind in config.user_association_cascade) if start_cascade: for k in config.user_association_cascade[to_kind]: # figure out the target entity attr = 'assc_{}_list'.format(k) # target_id = getattr(to_entity, attr)[0] target_list = getattr(to_entity, attr) if len(target_list) > 0: target_id = target_list[0] target_entity = core.Model.get_from_path(k, target_id) # only associate if the user isn't ALREADY associated if target_id not in getattr(from_entity, attr): from_entity = self.associate('associate', from_entity, target_entity, put=False) # else: we can't cascade because this entity's associations # aren't complete. This happens when they are first created, # and we don't have to worry about it. if not put: # avoid multiple db.puts when creating entities return from_entity else: from_entity.put() return from_entity
def unassociate(self, action, from_entity, to_entity, put=True): logging.info( 'Api.unassociate(action={}, from_entity={}, to_entity={}, put={})'. format(action, from_entity.id, to_entity.id, put)) # create the requested association from_kind = core.get_kind(from_entity) to_kind = core.get_kind(to_entity) if not self.user.can_associate(action, from_entity, to_entity): raise PermissionDenied() logging.info("action {}".format(action)) if action == 'disown': property_name = 'owned_' + to_kind + '_list' elif action == 'unassociate': property_name = 'assc_' + to_kind + '_list' relationship_list = getattr(from_entity, property_name) if to_entity.id in relationship_list: relationship_list.remove(to_entity.id) setattr(from_entity, property_name, relationship_list) # recurse through cascading relationships start_cascade = (action == 'unassociate' and from_kind == 'user' and to_kind in config.user_disassociation_cascade) if start_cascade: for k in config.user_disassociation_cascade[to_kind]: # figure out the target entity attr = 'assc_{}_list'.format(k) target_id = getattr(to_entity, attr)[0] target_entity = core.Model.get_from_path(k, target_id) # make sure the user is ALREADY associated with the target if target_id in getattr(from_entity, attr): # then actually unassociate from_entity = self.unassociate('unassociate', from_entity, target_entity, put=False) if not put: # avoid multiple db.puts when creating entities return from_entity else: from_entity.put() return from_entity
def delete(self, kind, id): logging.info('Api.delete(kind={}, id={})'.format(kind, id)) entity = core.Model.get_from_path(kind, id) if not self.user.has_permission('delete', entity): raise PermissionDenied() deleted_list = self._get_children(kind, id, [('deleted =', False)]) cache = {} for e in deleted_list: # IdModel entities have relationships and need to be disassociated # when they are deleted. NamedModel entities (e.g. ShortLink) # don't and we can skip this step. if isinstance(e, IdModel): cache = self._disassociate(e.id, cache=cache) e.deleted = True # save changes to deleted entities db.put(deleted_list) # save changes to users which were disassociated from deleted entities db.put([e for id, e in cache.items()]) return True
def create(self, kind, kwargs): logging.info('Api.create(kind={}, kwargs={})'.format(kind, kwargs)) logging.info("Api.create is in transction: {}".format( db.is_in_transaction())) # check permissions # can this user create this type of object? if not self.user.can_create(kind): raise PermissionDenied("User type {} cannot create {}".format( self.user.user_type, kind)) # if creating a user, can this user create this TYPE of user if kind == 'user': if not self.user.can_put_user_type(kwargs['user_type']): raise PermissionDenied( "{} cannot create users of type {}.".format( self.user.user_type, kwargs['user_type'])) # create the object klass = core.kind_to_class(kind) # some updates require additional validity checks if kind in config.custom_create: # These put and associate themselves; the user is sent in so custom # code can check permissions. entity = klass.create(self.user, **kwargs) return entity else: # non-custom creates require more work entity = klass.create(**kwargs) if kind in config.kinds_requiring_put_validation: entity.validate_put(kwargs) # create initial relationships with the creating user action = config.creator_relationships.get(kind, None) if action is not None: if self.user.user_type == 'public': raise Exception( "We should never be associating with the public user.") # associate, but don't put the created entity yet, there's more # work to do self.user = self.associate(action, self.user, entity, put=False) self.user.put() # do put the changes to the creator # create required relationships between the created entity and existing # non-user entities # different types of users have different required relationships k = kind if kind != 'user' else entity.user_type for kind_to_associate in config.required_associations.get(k, []): target_klass = core.kind_to_class(kind_to_associate) # the id of the entity to associate must have been passed in target = target_klass.get_by_id(kwargs[kind_to_associate]) entity = self.associate('associate', entity, target, put=False) if k in config.optional_associations: for kind_to_associate in config.optional_associations[k]: # they're optional, so check if the id has been passed in if kind_to_associate in kwargs: # if it was, do the association target_klass = core.kind_to_class(kind_to_associate) target = target_klass.get_by_id(kwargs[kind_to_associate]) entity = self.associate('associate', entity, target, put=False) # At one point we created qualtrics link pds for students here. Now # that happens in the program app via the getQualtricsLinks functional # node. # now we're done, so we can put all the changes to the new entity entity.put() return entity