class AcarsPosition(model.Model): flight_id = model.StringProperty() message_id = model.StringProperty() system_time = model.DateTimeProperty(auto_now_add=True) remote_time = model.DateTimeProperty() message_type = model.StringProperty() flight_status = model.IntegerProperty() waypoint = model.StringProperty() lat_lon = model.GeoPtProperty() hdg = model.IntegerProperty() alt = model.IntegerProperty() vs = model.IntegerProperty() gs = model.IntegerProperty() ias = model.IntegerProperty() tas = model.IntegerProperty() fob = model.IntegerProperty() wnd = model.StringProperty() oat = model.IntegerProperty() tat = model.IntegerProperty() distance_from_dept = model.IntegerProperty() distance_total = model.IntegerProperty() pause_mode = model.IntegerProperty() airport = model.StringProperty() message = model.TextProperty() def add_position(self): self.put_async() if self.lat_lon: acars_flight = AcarsFlight.query( AcarsFlight.flight_id == self.flight_id).get() flight_position = FlightPosition(lat_lon=self.lat_lon, altitude=self.alt) acars_flight.flight_path.append(flight_position) acars_flight.put_async()
class EnkiModelPost(model.Model): author = model.IntegerProperty() body = model.TextProperty() thread = model.IntegerProperty() # thread the post belongs to time_created = model.DateTimeProperty(auto_now_add=True) time_updated = model.DateTimeProperty(auto_now=True)
class EnkiModelUser(model.Model): #=== MODEL ==================================================================== # if logged in through enki auth, otherwise null email = model.StringProperty() # unique password = model.StringProperty() # if logged in through external provider at least once, otherwise null. Format "provider:userId" auth_ids_provider = model.StringProperty(repeated=True) # unique roles = model.StringProperty(repeated=True) time_created = model.DateTimeProperty(auto_now_add=True) time_updated = model.DateTimeProperty(auto_now=True) #=== QUERIES ================================================================== @classmethod def count(cls): count = EnkiModelUser.query().count() return count @classmethod def get_key_by_email(cls, email): return cls.query(cls.email == email).get(keys_only=True) @classmethod def get_by_email(cls, email): return cls.query(cls.email == email).get() @classmethod def exist_by_auth_id(cls, auth_id): count = cls.query(cls.auth_ids_provider == auth_id).count(1) return count > 0 @classmethod def get_by_auth_id(cls, auth_id): return cls.query(cls.auth_ids_provider == auth_id).get() #=== UTILITIES ================================================================ @classmethod def exist_by_email(cls, email): if email and email != 'removed': count = cls.query(cls.email == email).count(1) return count > 0 return False @classmethod def has_password_by_email(cls, email): user = cls.get_by_email(email) if user.password: return True return False
class EnkiModelUser(model.Model): # if logged in through enki auth, otherwise null email = model.StringProperty() # unique password = model.StringProperty() # if logged in through external provider at least once, otherwise null. Format "provider:userId" auth_ids_provider = model.StringProperty(repeated=True) # unique # other time_created = model.DateTimeProperty(auto_now_add=True) time_updated = model.DateTimeProperty(auto_now=True)
class EnkiModelApp(model.Model): user_id = model.IntegerProperty() name = model.StringProperty() secret = model.StringProperty() time_created = model.DateTimeProperty(auto_now_add=True) @classmethod def exist_by_name(cls, name): count = cls.query(cls.name == name).count(1) return count > 0 @classmethod def count_by_user_id(cls, user_id): return cls.query(cls.user_id == user_id).count() @classmethod def fetch_by_user_id(cls, user_id): list = cls.query(cls.user_id == user_id).order( cls.time_created).fetch() return list @classmethod def exist_by_app_id_app_secret(cls, app_id, app_secret): item = ndb.Key(cls, int(app_id)).get() if item and item.secret == app_secret: return True return False
class EnkiModelSummary(model.Model): #=== MODEL ==================================================================== name = model.StringProperty() count = model.IntegerProperty() time_created = model.DateTimeProperty(auto_now_add=True) #=== UTILITIES ================================================================ @classmethod def create(cls, name, count): cls(name=name, count=count).put_async() @classmethod def csv(cls): list = cls.query().order(-cls.time_created, cls.name).fetch() result = '"time_created","count","name"\n' for item in list: time_created = '"' + str(item.time_created).replace('"', "''") + '"' count = '"' + str(item.count) + '"' name = '"' + str(item.name).replace('"', "''") + '"' result += ','.join([time_created, count, name]) + '\n' return result
class EnkiModelTokenEmailRollback(model.Model): #=== MODEL ==================================================================== token = model.StringProperty() email = model.StringProperty() user_id = model.IntegerProperty() # ndb user ID time_created = model.DateTimeProperty(auto_now_add=True) #=== QUERIES ================================================================== @classmethod def get_by_user_id_email(cls, user_id, email): return cls.query(ndb.AND(cls.user_id == user_id, cls.email == email)).get() @classmethod def get_by_token(cls, token): return cls.query(cls.token == token).get() @classmethod def fetch_keys_by_user_id(cls, user_id): return cls.query(cls.user_id == user_id).fetch(keys_only=True) @classmethod def fetch_keys_by_user_id_time(cls, user_id, time_created): return cls.query( ndb.AND(cls.time_created >= time_created, cls.user_id == user_id)).fetch(keys_only=True)
class BaseStatistic(model.Model): """Base Statistic Model class. Attributes: bytes: the total number of bytes taken up in Cloud Datastore for the statistic instance. count: attribute is the total number of occurrences of the statistic in Cloud Datastore. timestamp: the time the statistic instance was written to Cloud Datastore. """ STORED_KIND_NAME = '__BaseStatistic__' bytes = model.IntegerProperty() count = model.IntegerProperty() timestamp = model.DateTimeProperty() @classmethod def _get_kind(cls): """Kind name override.""" return cls.STORED_KIND_NAME
class EnkiModelDisplayName( model.Model ): user_id = model.IntegerProperty() prefix = model.StringProperty() # prefix e.g. 'Jane' prefix_lower = model.ComputedProperty(lambda self: self.prefix.lower()) # lowercase prefix e.g. "jane" suffix = model.StringProperty() # suffix e.g. '#1234' => full display name = 'Jane#1234' current = model.BooleanProperty( default = True ) time_created = model.DateTimeProperty( auto_now_add = True )
class EnkiModelProductKey( model.Model ): licence_key = model.StringProperty() # mandatory product_name = model.StringProperty() # mandatory purchaser_email = model.StringProperty() # mandatory purchaser_user_id = model.IntegerProperty() # if the purchaser is registered shop_name = model.StringProperty() #choices = [ 'FastSpring' ]) purchase_price = model.StringProperty() quantity = model.IntegerProperty() order_id = model.StringProperty() order_type = model.StringProperty( choices = [ 'emulated', 'test', 'normal' ]) activated_by_user = model.IntegerProperty( ) time_created = model.DateTimeProperty( auto_now_add = True ) time_updated = model.DateTimeProperty( auto_now = True )
class EnkiModelRestAPIDataStore(model.Model): user_id = model.IntegerProperty() app_id = model.StringProperty() data_type = model.StringProperty() data_id = model.StringProperty() data_payload = model.JsonProperty() time_expires = model.DateTimeProperty(auto_now_add=False) read_access = model.StringProperty( choices=['private', 'friends', 'public'], default='private')
class AcarsPirep(model.Model): time_report = model.DateTimeProperty(auto_now_add=True) acars_id = model.IntegerProperty() user_id = model.StringProperty() flight_number = model.StringProperty() ac_icao = model.StringProperty() cruise_alt = model.IntegerProperty() flight_type = model.StringProperty() departure = model.StringProperty() destination = model.StringProperty() alternate = model.StringProperty() dep_time = model.DateTimeProperty() block_time = model.IntegerProperty() block_fuel = model.IntegerProperty() flight_time = model.IntegerProperty() flight_fuel = model.IntegerProperty() pax = model.IntegerProperty() cargo = model.IntegerProperty() online = model.IntegerProperty() engine_start_ts = model.IntegerProperty() takeoff_ts = model.IntegerProperty() landing_ts = model.IntegerProperty() engine_stop_ts = model.IntegerProperty() zero_fuel_weight = model.IntegerProperty() take_off_weight = model.IntegerProperty() landing_weight = model.IntegerProperty() out_geo = model.GeoPtProperty() out_altitude = model.IntegerProperty() in_geo = model.GeoPtProperty() in_altitude = model.IntegerProperty() max_climb_rate = model.IntegerProperty() max_descend_rate = model.IntegerProperty() max_ias = model.IntegerProperty() max_gs = model.IntegerProperty() def add_pirep(self): self.put_async() def flights_for_user(self, user_id, limit=20, offset=0): return AcarsPirep.query(AcarsPirep.user_id == user_id).fetch( limit, offset=offset) flights_for_user = classmethod(flights_for_user)
class EnkiModelThread(model.Model): author = model.IntegerProperty() title = model.StringProperty() forum = model.IntegerProperty() # forum the thread belongs to num_posts = model.IntegerProperty( default=0) # number of posts in the thread time_created = model.DateTimeProperty(auto_now_add=True)
class EnkiModelForum(model.Model): title = model.StringProperty() description = model.StringProperty() group = model.StringProperty() # group of forums order = model.IntegerProperty( default=0) # sort the forums (within a group) num_threads = model.IntegerProperty( default=0) # number of threads in the forum num_posts = model.IntegerProperty( default=0) # number of posts in the forum's threads time_created = model.DateTimeProperty(auto_now_add=True)
class Message(model.Model): username = model.StringProperty() picture_url = model.StringProperty() message = model.StringProperty() commit_hash = model.StringProperty(required=False) url = model.StringProperty(required=False) project = model.StringProperty(required=False) timestamp = model.DateTimeProperty(auto_now_add=True) def __str__(self): return '%s - %s' % (self.username, self.message) def __unicode__(self): return self.__str__() def to_json(self): data = self.to_dict(exclude=['timestamp']) return json.dumps(data) @classmethod def create_message(cls, username, picture_url, message, **kwargs): message = cls(username=username, picture_url=picture_url, message=message) message.populate(**kwargs) message.put() deferred.defer(send_live_message, message.key.urlsafe(), _queue="live") return message @classmethod def add_commit(cls, key): commit_key = model.Key(urlsafe=key) commit = commit_key.get() parent_key = commit_key.parent() if parent_key is None: return parent = parent_key.get() picture_url = getattr(parent, 'picture_url', '/static/images/spread_the_word_button.png') message = cls(username=parent.username, picture_url=picture_url, message=commit.message[:200], url=commit.url, project=commit.project, commit_hash=commit.hash) message.put() deferred.defer(send_live_message, message.key.urlsafe(), _queue="live") return message
class EnkiModelRestAPIConnectToken(model.Model): #=== MODEL ==================================================================== token = model.StringProperty() user_id = model.IntegerProperty() time_created = model.DateTimeProperty(auto_now_add=True) #=== CONSTANTS ================================================================ MAX_AGE = 5 # in minutes, duration of a connection token validity #=== QUERIES ================================================================== @classmethod def get_by_user_id_token_valid_age(cls, user_id, token): return cls.query( ndb.AND( cls.user_id == user_id, cls.token == token, cls.time_created > (datetime.datetime.now() - datetime.timedelta(minutes=cls.MAX_AGE)))).get() @classmethod def fetch_by_user(cls, user_id): return cls.query(cls.user_id == user_id).fetch(keys_only=True) @classmethod def fetch_expired(cls): return cls.query( cls.time_created < (datetime.datetime.now() - datetime.timedelta( minutes=cls.MAX_AGE))).fetch(keys_only=True) #=== UTILITIES ================================================================ @classmethod def cleanup_and_get_new_connection_token(cls, user_id): # note: ensure user is logged in and has display name before calling this function if user_id: # delete any existing connect token for the user ndb.delete_multi_async(cls.fetch_by_user(user_id)) # create a new token and return it token = enki.libutil.generate_connect_code() entity = cls(token=token, user_id=int(user_id)) entity.put() return token return None
class Change(ndb.Model): recordId = model.StringProperty() when = model.DateTimeProperty() subscriberId = model.StringProperty() kind = model.IntegerProperty() CHANGE_VOTE = 1 CHANGE_PLACE = 2 CHANGE_COMMENT = 3 @classmethod def migrate_old_votes_to_changes(cls): count = 0 vote_changes = VoteChange.query() for v in vote_changes: new_change = Change() new_change.kind = cls.CHANGE_VOTE new_change.subscriberId = v.subscriberId new_change.recordId = v.voteId new_change.when = v.when new_change.put() count += 1 return count @classmethod def migrate_old_places_to_changes(cls): count = 0 place_changes = PlaceChange.query() for p in place_changes: new_change = Change() new_change.kind = cls.CHANGE_PLACE new_change.subscriberId = p.subscriberId new_change.recordId = p.placeId new_change.when = p.when new_change.put() count += 1 return count
class EnkiModelRestAPIConnectToken(model.Model): token = model.StringProperty() user_id = model.IntegerProperty() time_created = model.DateTimeProperty(auto_now_add=True)
class EnkiModelMessage(model.Model): #=== MODEL ==================================================================== sender = model.IntegerProperty() recipient = model.IntegerProperty() type = model.StringProperty() time_created = model.DateTimeProperty(auto_now_add=True) #=== QUERIES ================================================================== @classmethod def get_by_id(cls, message_id): return ndb.Key(cls, message_id).get() @classmethod def exist_by_recipient(cls, user_id): count = cls.query(cls.recipient == user_id).count(1) return count > 0 @classmethod def count_by_recipient(cls, user_id): return cls.query(cls.recipient == user_id).count() @classmethod def fetch_by_recipient(cls, user_id): return cls.query(cls.recipient == user_id).fetch() @classmethod def exist_by_sender_recipient(cls, sender_id, recipient_id): count = cls.query( ndb.AND(cls.sender == sender_id, cls.recipient == recipient_id)).count(1) return count > 0 @classmethod def get_key_by_sender_recipient(cls, sender_id, recipient_id): return cls.query( ndb.AND(cls.sender == sender_id, cls.recipient == recipient_id)).get(keys_only=True) @classmethod def get_by_sender_recipient(cls, sender_id, recipient_id): return cls.query( ndb.AND(cls.sender == sender_id, cls.recipient == recipient_id)).get() @classmethod def exist_sent_or_received(cls, user_id): count = cls.query( ndb.OR(cls.sender == user_id, cls.recipient == user_id)).count(1) return count > 0 @classmethod def fetch_keys_sent_or_received(cls, user_id): return cls.query( ndb.OR(cls.sender == user_id, cls.recipient == user_id)).fetch(keys_only=True) #=== UTILITIES ================================================================ @classmethod def send_message(cls, sender_id, recipient_id, type): message = EnkiModelMessage(sender=sender_id, recipient=recipient_id, type=type) message.put() @classmethod def get_messages(cls, user_id): list = cls.fetch_by_recipient(user_id) message_list = [] if list: for i, item in enumerate(list): entity = EnkiModelDisplayName.get_by_user_id_current( item.sender) sender = EnkiModelDisplayName.get_user_id_display_name_url( entity) type = item.type message_id = item.key.id() message = messageData(message_id, type, sender) message_list.append(message) return message_list @classmethod def remove_message(cls, message_id): message = cls.get_by_id(message_id) if message: message.key.delete() @classmethod def remove_messages_crossed(cls, sender_or_receiver_a_id, sender_or_receiver_b_id): message_a = cls.get_by_sender_recipient(sender_or_receiver_a_id, sender_or_receiver_b_id) message_b = cls.get_by_sender_recipient(sender_or_receiver_b_id, sender_or_receiver_a_id) if message_a: if message_a.type == 'friend_request': message_a.key.delete() if message_b: if message_b.type == 'friend_request': message_b.key.delete() @classmethod def delete_user_messages(cls, user_id): ndb.delete_multi(cls.fetch_keys_sent_or_received(user_id))
class EnkiModelTokenAuth( model.Model ): token = model.StringProperty() # unique user_id = model.IntegerProperty() # the ndb ID nr time_created = model.DateTimeProperty( auto_now_add = True )
class BlobInfo(model.Model): """Information about blobs in Blobstore. This is a Model subclass that has been doctored to be unwritable. Properties: - content_type: Content type of blob. - creation: Creation date of blob, when it was uploaded. - filename: Filename user selected from their machine. - size: Size of uncompressed blob. - md5_hash: The md5 hash value of the uploaded blob (in hex). Additional API: Class methods: - get(): retrieve a BlobInfo by key - get_multi(): retrieve a list of BlobInfos by keys - get_async(), get_multi_async(): async version of get() and get_multi() Instance methods: - delete(): delete this blob - delete_async(): async version of delete() - key(): return the BlobKey for this blob - open(): return a BlobReader instance for this blob Because BlobInfo instances are synchronized with Blobstore, the class cache policies are off. Do not subclass this class. """ _use_cache = False _use_memcache = False content_type = model.StringProperty() creation = model.DateTimeProperty() filename = model.StringProperty() size = model.IntegerProperty() md5_hash = model.StringProperty() @classmethod def _get_kind(cls): """Override this to match the datastore entities written by Blobstore.""" return BLOB_INFO_KIND @classmethod def get(cls, blob_key, **ctx_options): """Retrieve a BlobInfo by key. Args: blob_key: A blob key. This may be a str, unicode or BlobKey instance. **ctx_options: Context options for Model().get_by_id(). Returns: A BlobInfo entity associated with the provided key, If there was no such entity, returns None. """ fut = cls.get_async(blob_key, **ctx_options) return fut.get_result() @classmethod def get_async(cls, blob_key, **ctx_options): """Async version of get().""" if not isinstance(blob_key, (BlobKey, six.text_type, six.binary_type)): raise TypeError('Expected blob key, got %r' % (blob_key, )) if 'parent' in ctx_options: raise TypeError('Parent is not supported') blob_key_id = str(blob_key) if isinstance(blob_key, BlobKey) else blob_key return cls.get_by_id_async(six.ensure_binary(blob_key_id), **ctx_options) @classmethod def get_multi(cls, blob_keys, **ctx_options): """Multi-key version of get(). Args: blob_keys: A list of blob keys. **ctx_options: Context options for Model().get_by_id(). Returns: A list whose items are each either a BlobInfo entity or None. """ futs = cls.get_multi_async(blob_keys, **ctx_options) return [fut.get_result() for fut in futs] @classmethod def get_multi_async(cls, blob_keys, **ctx_options): """Async version of get_multi().""" for blob_key in blob_keys: if not isinstance(blob_key, (BlobKey, six.string_types)): raise TypeError('Expected blob key, got %r' % (blob_key, )) if 'parent' in ctx_options: raise TypeError('Parent is not supported') blob_key_strs = list(map(str, blob_keys)) keys = [model.Key(BLOB_INFO_KIND, id) for id in blob_key_strs] return model.get_multi_async(keys, **ctx_options) def _put_async(self, **ctx_options): """Cheap way to make BlobInfo entities read-only.""" raise TypeError('BlobInfo is read-only') put_async = _put_async def key(self): """Get key for blob. Returns: BlobKey instance that identifies this blob. """ return BlobKey(self._key.id()) def delete(self, **options): """Permanently delete this blob from Blobstore. Args: **options: Options for create_rpc(). """ fut = delete_async(self.key(), **options) fut.get_result() def delete_async(self, **options): """Async version of delete().""" return delete_async(self.key(), **options) def open(self, *args, **kwds): """Returns a BlobReader for this blob. Args: *args, **kwargs: Passed to BlobReader constructor. Returns: A BlobReader instance. """ return BlobReader(self, *args, **kwds)
class EnkiModelBackoffTimer(model.Model): # protect password entry against brute force attack identifier = model.StringProperty() last_failure = model.DateTimeProperty() backoff_duration = TimeDeltaProperty()
class User(model.Expando): """Stores user authentication credentials or authorization ids.""" #: The model used to ensure uniqueness. unique_model = Unique #: The model used to store tokens. token_model = UserToken created = model.DateTimeProperty(auto_now_add=True) updated = model.DateTimeProperty(auto_now=True) # ID for third party authentication, e.g. 'google:username'. UNIQUE. auth_ids = model.StringProperty(repeated=True) # Hashed password. Not required because third party authentication # doesn't use password. password = model.StringProperty() def get_id(self): """Returns this user's unique ID, which can be an integer or string.""" return self._key.id() @classmethod def get_by_auth_id(cls, auth_id): """Returns a user object based on a auth_id. :param auth_id: String representing a unique id for the user. Examples: - own:username - google:username :returns: A user object. """ return cls.query(cls.auth_ids == auth_id).get() @classmethod def get_by_auth_token(cls, user_id, token): """Returns a user object based on a user ID and token. :param user_id: The user_id of the requesting user. :param token: The token string to be verified. :returns: A tuple ``(User, timestamp)``, with a user object and the token timestamp, or ``(None, None)`` if both were not found. """ token_key = cls.token_model.get_key(user_id, 'auth', token) user_key = model.Key(cls, user_id) # Use get_multi() to save a RPC call. valid_token, user = model.get_multi([token_key, user_key]) if valid_token and user: timestamp = int(time.mktime(valid_token.created.timetuple())) return user, timestamp return None, None @classmethod def get_by_auth_password(cls, auth_id, password): """Returns a user object, validating password. :param auth_id: Authentication id. :param password: Password to be checked. :returns: A user object, if found and password matches. :raises: ``auth.InvalidAuthIdError`` or ``auth.InvalidPasswordError``. """ user = cls.get_by_auth_id(auth_id) if not user: raise auth.InvalidAuthIdError() if not security.check_password_hash(password, user.password): raise auth.InvalidPasswordError() return user @classmethod def validate_token(cls, user_id, subject, token): """Checks for existence of a token, given user_id, subject and token. :param user_id: User unique ID. :param subject: The subject of the key. Examples: - 'auth' - 'signup' :param token: The token string to be validated. :returns: A :class:`UserToken` or None if the token does not exist. """ return cls.token_model.get(user=user_id, subject=subject, token=token) is not None @classmethod def create_auth_token(cls, user_id): """Creates a new authorization token for a given user ID. :param user_id: User unique ID. :returns: A string with the authorization token. """ return cls.token_model.create(user_id, 'auth').token @classmethod def validate_auth_token(cls, user_id, token): return cls.validate_token(user_id, 'auth', token) @classmethod def delete_auth_token(cls, user_id, token): """Deletes a given authorization token. :param user_id: User unique ID. :param token: A string with the authorization token. """ cls.token_model.get_key(user_id, 'auth', token).delete() @classmethod def create_signup_token(cls, user_id): entity = cls.token_model.create(user_id, 'signup') return entity.token @classmethod def validate_signup_token(cls, user_id, token): return cls.validate_token(user_id, 'signup', token) @classmethod def delete_signup_token(cls, user_id, token): cls.token_model.get_key(user_id, 'signup', token).delete() @classmethod def create_user(cls, auth_id, unique_properties=None, **user_values): """Creates a new user. :param auth_id: A string that is unique to the user. Users may have multiple auth ids. Example auth ids: - own:username - own:[email protected] - google:username - yahoo:username The value of `auth_id` must be unique. :param unique_properties: Sequence of extra property names that must be unique. :param user_values: Keyword arguments to create a new user entity. Since the model is an ``Expando``, any provided custom properties will be saved. To hash a plain password, pass a keyword ``password_raw``. :returns: A tuple (boolean, info). The boolean indicates if the user was created. If creation succeeds, ``info`` is the user entity; otherwise it is a list of duplicated unique properties that caused creation to fail. """ assert user_values.get('password') is None, \ 'Use password_raw instead of password to create new users.' assert not isinstance(auth_id, list), \ 'Creating a user with multiple auth_ids is not allowed, ' \ 'please provide a single auth_id.' if 'password_raw' in user_values: user_values['password'] = security.generate_password_hash( user_values.pop('password_raw'), length=12) user_values['auth_ids'] = [auth_id] user = cls(**user_values) # Set up unique properties. uniques = [('%s.auth_id:%s' % (cls.__name__, auth_id), 'auth_id')] if unique_properties: for name in unique_properties: key = '%s.%s:%s' % (cls.__name__, name, user_values[name]) uniques.append((key, name)) ok, existing = cls.unique_model.create_multi(k for k, v in uniques) if ok: user.put() return True, user else: properties = [v for k, v in uniques if k in existing] return False, properties
class VoteChange(ndb.Model): voteId = model.StringProperty() when = model.DateTimeProperty() subscriberId = model.StringProperty()
class EnkiModelTokenVerify(model.Model): token = model.StringProperty() email = model.StringProperty() user_id = model.IntegerProperty() # ndb user ID time_created = model.DateTimeProperty(auto_now_add=True) type = model.StringProperty() auth_ids_provider = model.StringProperty( ) # store auth Id info for registration @classmethod def get_by_token(cls, token): entity = cls.query(cls.token == token).get() return entity @classmethod def get_by_token_type(cls, token, type): entity = cls.query(ndb.AND(cls.token == token, cls.type == type)).get() return entity @classmethod def get_by_user_id_email_type(cls, user_id, email, type): entity = cls.query( ndb.AND(cls.user_id == user_id, cls.email == email, cls.type == type)).get() return entity @classmethod def get_by_user_id_auth_id_type(cls, user_id, auth_id, type): entity = cls.query( ndb.AND(cls.user_id == user_id, cls.auth_ids_provider == auth_id, cls.type == type)).get() return entity @classmethod def get_by_user_id_type(cls, user_id, type): entity = cls.query(ndb.AND(cls.user_id == user_id, cls.type == type)).get() return entity @classmethod def get_by_auth_id_type(cls, auth_id, type): entity = cls.query( ndb.AND(cls.auth_ids_provider == auth_id, cls.type == type)).get() return entity @classmethod def fetch_by_user_id_type(cls, user_id, type): list = cls.query( ndb.AND(cls.user_id == user_id, cls.type == type)).order(-cls.time_created).fetch() return list @classmethod def fetch_keys_by_user_id_type(cls, user_id, type): keys = cls.query(ndb.AND(cls.user_id == user_id, cls.type == type)).fetch(keys_only=True) return keys @classmethod def fetch_keys_by_user_id_except_type(cls, user_id, type): keys = cls.query(ndb.AND(cls.user_id == user_id, cls.type != type)).fetch(keys_only=True) return keys @classmethod def fetch_keys_by_email_type(cls, email, type): keys = cls.query(ndb.AND(cls.email == email, cls.type == type)).fetch(keys_only=True) return keys @classmethod def exist_by_token_type(cls, token, type): count = cls.query(ndb.AND(cls.token == token, cls.type == type)).count(1) return count > 0 @classmethod def exist_by_user_id_token(cls, user_id, token): count = cls.query(ndb.AND(cls.user_id == user_id, cls.token == token)).count(1) return count > 0 @classmethod def delete_by_user_id_token(cls, user_id, token): key = cls.query(ndb.AND(cls.user_id == user_id, cls.token == token)).fetch(keys_only=True) if key: key[0].delete() return True return False @classmethod def delete_token_by_id(cls, token_id): ndb.Key(cls, int(token_id)).delete() @classmethod def fetch_old_tokens_by_types(cls, days_old, types): list = cls.query( ndb.AND( cls.type.IN(types), cls.time_created <= (datetime.datetime.now() - datetime.timedelta(days=days_old)))).fetch(keys_only=True) return list
class EnkiModelForum(model.Model): #=== MODEL ==================================================================== title = model.StringProperty() description = model.StringProperty() group = model.StringProperty() # group of forums group_order = model.IntegerProperty( default=0) # order the groups appear in on the page forum_order = model.IntegerProperty( default=0) # order the forums appear in within a group num_threads = model.IntegerProperty( default=0) # number of threads in the forum num_posts = model.IntegerProperty( default=0) # number of posts in the forum's threads time_created = model.DateTimeProperty(auto_now_add=True) time_updated = model.DateTimeProperty(auto_now=True) #=== QUERIES ================================================================== @classmethod def exist(cls): count = cls.query().count(1) return count > 0 @classmethod def fetch(cls): return cls.query().order(cls.group_order, cls.forum_order).fetch() #=== UTILITIES ================================================================ @classmethod def create_forums(cls): # create an executable string from the forums settings to add the forums expression_forum = '''enki.modelforum.EnkiModelForum( group_order = {group_order}, forum_order = {forum_order}, group = "{group}", title = "{title}", description = "{description}" ), ''' expression = "ndb.put_multi([ " increment = 10 group_order = 0 forum_order = 0 current_group = '' for index, item in enumerate(settings.FORUMS): if item[0] != current_group: # new group: increment the group order index and reset the forum order index current_group = item[0] group_order += increment forum_order = increment else: forum_order += increment expression += expression_forum.format(group_order=group_order, forum_order=forum_order, group=current_group, title=item[1], description=item[2]) expression += " ])" exec(expression) @classmethod def get_forums_data(cls): forums_data = [] forums_list = cls.fetch() if forums_list: # get the groups from the list (ordered) groups = [] for forum in forums_list: if forum.group not in groups: groups.append(forum.group) # get the forums for each group (ordered) for group in groups: group_num_threads = 0 group_num_posts = 0 forums = [] for forum in forums_list: if forum.group == group: group_num_threads += forum.num_threads group_num_posts += forum.num_posts url = enki.libutil.get_local_url( 'forum', {'forum': str(forum.key.id())}) forums.append({ 'title': forum.title, 'description': forum.description, 'time_updated': forum.time_updated, 'num_threads': forum.num_threads, 'num_posts': forum.num_posts, 'url': url }) forums_data.append({ 'name': group, 'num_threads': group_num_threads, 'num_posts': group_num_posts, 'forums': forums }) return forums_data
class EnkiModelMessage(model.Model): sender = model.IntegerProperty() recipient = model.IntegerProperty() type = model.StringProperty() time_created = model.DateTimeProperty(auto_now_add=True)
class PlaceChange(ndb.Model): placeId = model.StringProperty() when = model.DateTimeProperty() subscriberId = model.StringProperty()
class EnkiModelEmailSubscriptions(model.Model): #=== MODEL ==================================================================== email = model.StringProperty() newsletters = model.StringProperty(repeated=True) token = model.StringProperty() # unsubscribe token time_created = model.DateTimeProperty(auto_now_add=True) time_updated = model.DateTimeProperty(auto_now=True) #=== CONSTANTS ================================================================ #=== QUERIES ================================================================== @classmethod def exist_by_email(cls, email): count = cls.query(cls.email == email).count(1) return count > 0 @classmethod def get_by_email(cls, email): return cls.query(cls.email == email).get() @classmethod def get_by_token(cls, token): return cls.query(cls.token == token).get() @classmethod def exist_by_email_newsletter(cls, email, newsletter): count = cls.query( ndb.AND(cls.email == email, cls.newsletters == newsletter)).count(1) return count > 0 @classmethod def get_by_email_newsletter(cls, email, newsletter): return cls.query( ndb.AND(cls.email == email, cls.newsletters == newsletter)).get() @classmethod def exist_by_token_newsletter(cls, token, newsletter): count = cls.query( ndb.AND(cls.token == token, cls.newsletters == newsletter)).count(1) return count > 0 @classmethod def fetch_keys_by_email(cls, email): return cls.query(cls.email == email).fetch(keys_only=True) @classmethod def count(cls): return cls.query().count() #=== UTILITIES ================================================================ @classmethod def add_newsletter(cls, email, newsletter): entity = cls.get_by_email_newsletter(email, newsletter) if entity: return entity.token else: existing_entity = cls.get_by_email(email) if existing_entity: # add the newsletter to the list existing_entity.newsletters.append(newsletter) existing_entity.put() return existing_entity.token else: # create a new entity token = security.generate_random_string(entropy=256) new_entity = cls(email=email, newsletters=[newsletter], token=token) new_entity.put() return token @classmethod def remove_newsletter_by_email(cls, email, newsletter): if cls.exist_by_email_newsletter(email, newsletter): entity = cls.get_by_email(email) if len(entity.newsletters) == 1: # if no newsletter would be left after removal, delete the record entity.key.delete() else: # remove the newsletter. index = entity.newsletters.index(newsletter) del entity.newsletters[index] entity.put() @classmethod def remove_newsletter_by_token(cls, token, newsletter): if cls.exist_by_token_newsletter(token, newsletter): entity = cls.get_by_token(token) if len(entity.newsletters) == 1: # if no newsletter would be left after removal, delete the record entity.key.delete() else: # remove the newsletter index = entity.newsletters.index(newsletter) del entity.newsletters[index] entity.put() @classmethod def remove_by_email(cls, email): entities = cls.fetch_keys_by_email(email) if entities: ndb.delete_multi_async(entities) @classmethod def count_newsletters_by_email(cls, email): count = 0 entity = cls.get_by_email(email) if entity: count = len(entity.newsletters) return count @classmethod def get_mailgun_email_batches(cls, newsletter): # get batches of email addresses and their respective recipient variables (unsubscribe tokens) for a given newsletter. # reference: https://documentation.mailgun.com/en/latest/user_manual.html#batch-sending BATCH_SIZE = 1000 batches_emails = [] batches_emails_recipient_variables = [] results, next_cursor, more = cls.query( cls.newsletters == newsletter).fetch_page(BATCH_SIZE) batch_emails = '' batch_emails_recipient_variables = {} for result in results: batch_emails += result.email + ', ' batch_emails_recipient_variables[result.email] = { 'token': result.token } batches_emails.append(batch_emails[:-2]) batches_emails_recipient_variables.append( batch_emails_recipient_variables) while (more and next_cursor): cursor = next_cursor results, next_cursor, more = cls.query( cls.newsletters == newsletter).fetch_page(BATCH_SIZE, start_cursor=cursor) batch_emails = '' batch_emails_recipient_variables = {} for result in results: batch_emails += result.email + ', ' batch_emails_recipient_variables[result.email] = { 'token': result.token } batches_emails.append(batch_emails[:-2]) batches_emails_recipient_variables.append( batch_emails_recipient_variables) return batches_emails, batches_emails_recipient_variables
class Fact(model.Model): author = model.StringProperty() poster_ip = model.StringProperty() posted_on = model.DateTimeProperty(auto_now_add=True) last_changed = model.DateTimeProperty(auto_now=True) text = model.TextProperty() language = model.StringProperty(default='en') # The language of the post # For selecting random instances random_index = model.ComputedProperty( lambda self: random.randint(0, sys.maxint)) # For the Elo rating system # see http://en.wikipedia.org/wiki/Elo_rating_system total_opponent_ratings = model.FloatProperty(default=0.) wins = model.IntegerProperty(default=0) losses = model.IntegerProperty(default=0) games = model.IntegerProperty(default=0) elo_rating = model.FloatProperty(default=400.) @property def k_factor(self): "Gives the correction (K) factor" if self.elo_rating <= 2100: return 32. elif self.elo_rating <= 2400: return 24. else: return 16. def expected_chance_against(self, fact): "Gives the expected odds of this fact winning a match with fact" return 1 / (1 + 10**((self.elo_rating + fact.elo_rating) / 400.)) @tasklets.tasklet def won_over(self, fact): """ Self won a match over another fact. Recalculates Elo ratings and saves them fact1.won_over(fact2) """ if self.key == fact.key: raise ValueError('A fact cannot compete with itself') previous_elo_rating = self.elo_rating # +------+-----+ # |Result|Score| # +------+-----+ # |Win |1 | # +------+-----+ # |Draw |0.5 | # +------+-----+ # |Loss |0 | # +------+-----+ self.elo_rating = self.elo_rating + self.k_factor * \ (1 - self.expected_chance_against(fact)) self.total_opponent_ratings += fact.elo_rating self.games += 1 self.wins += 1 f1 = self.put_async() fact.elo_rating = fact.elo_rating - fact.k_factor * \ (1 - fact.expected_chance_against(self)) # TODO: check if Elo ratings can become negative fact.elo_rating = 0 if fact.elo_rating < 0 else fact.elo_rating fact.total_opponent_ratings += previous_elo_rating fact.games += 1 fact.losses += 1 f2 = fact.put_async() yield f1, f2 def sync_won_over(self, fact): """ Self won a match over another fact. Recalculates Elo ratings and saves them, but do it synchronously fact1.sync_won_over(fact2) """ if self.key == fact.key: raise ValueError('A fact cannot compete with itself') previous_elo_rating = self.elo_rating self.elo_rating = self.elo_rating + self.k_factor * \ (1 - self.expected_chance_against(fact)) self.total_opponent_ratings += fact.elo_rating self.games += 1 self.wins += 1 self.put() fact.elo_rating = fact.elo_rating - fact.k_factor * \ (1 - fact.expected_chance_against(self)) # TODO: check if Elo ratings can become negative fact.elo_rating = 0 if fact.elo_rating < 0 else fact.elo_rating fact.total_opponent_ratings += previous_elo_rating fact.games += 1 fact.losses += 1 f2 = fact.put() @classmethod def random(cls, exclude=[]): "Returns a random instance" # TODO: do this asychronously f = None while not f: position = random.randint(1, sys.maxint) f = cls.query(cls.random_index >= position).get() if f and f.key in [e.key for e in exclude]: logging.error('got an excluded: ' + str(f)) f = None # Try again return f @classmethod def random_pair(cls): "Returns two random distinct facts" # TODO: do this asynchronously pos1 = random.randint(1, sys.maxint) f1 = cls.query(cls.random_index <= pos1).get() if not f1: f1 = cls.query(cls.random_index > pos1).get() pos2 = random.randint(1, sys.maxint) f2 = cls.query(cls.random_index <= pos1).get() if not f2: f2 = cls.query(cls.random_index > pos1).get() # f1 and f2 only must be resolved here if f1 == f2: f2 = cls.random(exclude=[f1]) return (f1, f2)