class SoftDeletableModel(models.Model): """ An abstract base class model with a ``is_removed`` field that marks entries that are not going to be used anymore, but are kept in db for any reason. Default manager returns only not-removed entries. """ is_removed = models.BooleanField(default=False) class Meta: abstract = True objects = SoftDeletableManager(_emit_deprecation_warnings=True) available_objects = SoftDeletableManager() all_objects = models.Manager() def delete(self, using=None, soft=True, *args, **kwargs): """ Soft delete object (set its ``is_removed`` field to True). Actually delete object if setting ``soft`` to False. """ if soft: self.is_removed = True self.save(using=using) else: return super().delete(using=using, *args, **kwargs)
class PDFDoc(CommonApprovableModel): title = models.CharField(verbose_name=_('Title'), max_length=200, unique=True) description = models.TextField(verbose_name=_('Description'), blank=True, null=True) download_count = models.PositiveIntegerField(null=True, verbose_name=_('Download Count'), default=0) author = models.CharField(max_length=100, null=True, blank=True, verbose_name=_('Author')) publish_year = models.DateField(null=True, blank=True, verbose_name=_('Publish Year')) objects = models.Manager() exist_objects = SoftDeletableManager() approved_pdf = ApprovedManager() class Meta: ordering = ["-upload_time"] def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.title, allow_unicode=True) super(PDFDoc, self).save(*args, **kwargs) def canShow(self): return self.approve_status == 'approved' def get_absolute_url(self): return reverse('showcase:detail_pdf', args=[self.slug]) @property def user_link(self): return self.get_absolute_url() def __str__(self): return self.title
class ApiKey(TimeStampedModel, SoftDeletableModel): api_key = models.CharField(max_length=50, null=False, blank=True, unique=True) live = ApiKeyManager() objects = SoftDeletableManager() removed = QueryManager(is_removed=True) @ecached_property('is_expire:{self.id}', 60) def is_expire(self): try: info = blockcypher.get_token_info(self.api_key) limits = info.get('limits', None) hits_history = info.get('hits_history', None) if not limits: return True if not hits_history: return False else: current_api_hour = sum( [i['api/hour'] for i in hits_history if 'api/hour' in i]) current_hooks_hour = sum([ i['hooks/hour'] for i in hits_history if 'hooks/hour' in i ]) if current_api_hour < limits['api/hour'] and \ current_hooks_hour < limits['hooks/hour']: return False except: return True def __str__(self): return self.api_key
class VideoAlbum(ApprovableModel, SoftDeletableModel): title = models.CharField(max_length=100, verbose_name=_('Title'), unique=True) description = models.TextField(blank=True, null=True, verbose_name=_('Description')) slug = models.CharField(unique=True, null=True, blank=True, db_index=True, max_length=100, \ validators=[validators.validate_unicode_slug]) images = GenericRelation(Image, related_query_name='imaged_video_album', verbose_name=_('Public Image')) objects = models.Manager() exist_objects = SoftDeletableManager() approved_albums = ApprovedManager() class Meta: ordering = ["-upload_time"] def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.title, allow_unicode=True) super(VideoAlbum, self).save(*args, **kwargs) def __str__(self): return self.title def get_image(self): return self.images.last() def exist_videos(self): return self.album_videos.filter(is_removed=0)
class BaseModel(TimeStampedModel, StatusModel, SoftDeletableModel): STATUS = Choices('draft', 'published') objects = SoftDeletableManager() class Meta: abstract = True
class License(ExtendedModel): SIGNALS_MAP = { 'updated': (update_related_datasets, ), 'published': (update_related_datasets, ), 'restored': (update_related_datasets, ), 'removed': (null_in_related_datasets, ), } name = models.CharField(max_length=200, verbose_name=_('Name')) title = models.CharField(max_length=250, verbose_name=_('Title')) url = models.URLField(blank=True, null=True, verbose_name=_('URL')) def __str__(self): return self.title i18n = TranslationField(fields=("name", "title")) raw = models.Manager() objects = SoftDeletableManager() deleted = DeletedManager() tracker = FieldTracker() slugify_field = 'name' @classmethod def accusative_case(cls): return _("acc: License") class Meta: verbose_name = _('License') verbose_name_plural = _('Licenses') default_manager_name = "objects" indexes = [ GinIndex(fields=["i18n"]), ]
class Transaction(TimeStampedModel, SoftDeletableModel): reference = models.CharField(max_length=255) wallet = models.ForeignKey('wallet.Wallet', related_name='transactions', on_delete=models.PROTECT) currency = models.CharField( max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH, default=settings.DEFAULT_CURRENCY, ) transaction_amount = models.DecimalField( max_digits=settings.DEFAULT_MAX_DIGITS, decimal_places=settings.DEFAULT_DECIMAL_PLACES, ) amount = MoneyField(amount_field="transaction_amount", currency_field="currency") is_pending = models.BooleanField(default=False) confirmed_at = models.DateTimeField(null=True, blank=True) objects = SoftDeletableManager() removed = QueryManager(is_removed=True) class Meta: ordering = ['wallet'] def __str__(self): return '({})'.format(self.wallet)
class AppVersion(SoftDeletableModel): version_number = models.CharField(max_length=10, validators=[validators.RegexValidator("[a-zA-Z0-9\.]*")], verbose_name=_('Version No.')) created_time = models.DateTimeField(auto_now_add=True, verbose_name=_('Created Time'), db_index=True) approve_status = models.CharField(max_length=10, choices=APPROVE_CHOICES, default='new', verbose_name=_('Approve Status')) approved_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_('Approved By')) approved_time = models.DateTimeField(null=True, blank=True, verbose_name=_('Approved Time')) mobile_app = models.ForeignKey(MobileApp, on_delete=models.CASCADE, blank=True, null=True, verbose_name=_('Application'), related_name='versions') whats_new = models.TextField(blank=True, null=True, verbose_name=_("What's New")) translator = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Translator")) android_version = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Supported Android Version")) remark = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Remark")) objects = models.Manager() exist_objects = SoftDeletableManager() approved_manager = ApprovedManager() class Meta: ordering = ["-created_time"] unique_together = (('mobile_app', 'version_number'),) get_latest_by = "created_time" def __str__(self): return self.version_number
class InspiredVideo(CommonApprovableModel): video = GenericRelation(Video, related_query_name='video_controller', verbose_name=_('video')) title = models.CharField(max_length=100, verbose_name=_('title'), unique=True) description = models.TextField(blank=True, null=True, verbose_name=_('description')) album = models.ForeignKey(VideoAlbum, null=True, blank=True, verbose_name=_("Album"),on_delete=models.CASCADE,\ related_name='album_videos') view_count = models.PositiveIntegerField(verbose_name=_("View Count"), default=0) images = GenericRelation(Image, related_query_name='thumnail_video') objects = models.Manager() exist_objects = SoftDeletableManager() approved_videos = ApprovedManager() shown_videos = ShownInspiredVideoManager() class Meta: ordering = ["-upload_time"] def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.title, allow_unicode=True) super(InspiredVideo, self).save(*args, **kwargs) def latest_valid_video(self): return self.video.exclude(file__isnull=True).exclude(file__exact='').last() def thumbnail(self): return self.images.last() def thumbnail_path(self): return str(self.thumbnail() or '') def __str__(self): return self.title @property def userLink(self): return reverse('showcase:detail_inspired_video', args=[self.slug]) @property def user_link_mobile(self): return reverse('showcase:detail_inspired_video', args=[self.slug]) def video_duration(self): latest_video = self.latest_valid_video() return latest_video.duration if latest_video else 0 @property def video_duration_str(self): seconds = self.video_duration() return str(datetime.timedelta(seconds=seconds)) def get_absolute_url(self): return reverse('showcase:detail_inspired_video', args=[self.slug])
class ArticleCategory(ExtendedModel): SIGNALS_MAP = { 'updated': (update_related_articles, ), 'published': (update_related_articles, ), 'restored': (update_related_articles, ), 'removed': (update_related_articles, ), } name = models.CharField(max_length=100, unique=True, verbose_name=_("name")) description = models.CharField(max_length=500, blank=True, verbose_name=_("Description")) created_by = models.ForeignKey(User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Created by"), related_name='article_categories_created') modified_by = models.ForeignKey(User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Modified by"), related_name='article_categories_modified') def __str__(self): return self.name_i18n i18n = TranslationField(fields=("name", "description")) tracker = FieldTracker() slugify_field = 'name' raw = models.Manager() objects = SoftDeletableManager() deleted = DeletedManager() class Meta: verbose_name = _("Article Category") verbose_name_plural = _("Article Categories") db_table = "article_category" default_manager_name = "objects" indexes = [ GinIndex(fields=['i18n']), ]
class Category(StatusModel, SoftDeletableModel, TimeStampedModel): STATUS = Choices(*STATUS_CHOICES) slug = models.SlugField(max_length=100, unique=True) # TODO: change to VarChar(100) title = models.TextField(verbose_name=_("Title")) description = models.TextField(null=True, verbose_name=_("Description")) color = models.CharField(max_length=20, default="#000000", null=True, verbose_name=_("Color")) image = models.ImageField(max_length=200, storage=storages.get_storage('common'), upload_to='', blank=True, null=True, verbose_name=_("Image URL")) created_by = models.ForeignKey(User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Created by"), related_name='categories_created') modified_by = models.ForeignKey(User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Modified by"), related_name='categories_modified') raw = models.Manager() objects = SoftDeletableManager() deleted = DeletedManager() class Meta: db_table = "category" verbose_name = _("Category") verbose_name_plural = _("Categories") default_manager_name = "objects" @classmethod def accusative_case(cls): return _("acc: Category") def __str__(self): return self.slug
class BasePage(SoftDeletableModel): title = models.CharField(max_length=50) content = RichTextField(blank=True) creator = models.ForeignKey(Account) creation_date = models.DateTimeField(auto_now_add=True) group_id = models.UUIDField(primary_key=False, default=uuid.uuid4, editable=False) version = models.IntegerField() changelog = models.CharField(max_length=50, blank=True) objects = Manager() active_pages = SoftDeletableManager() class Meta: abstract = True @property def category(self): return self.__class__.__name__
class SoftDeletableModel(models.Model): """ An abstract base class model with a ``is_removed`` field that marks entries that are not going to be used anymore, but are kept in db for any reason. Default manager returns only not-removed entries. """ is_removed = models.BooleanField(default=False) class Meta: abstract = True objects = SoftDeletableManager() def delete(self, using=None, keep_parents=False): """ Soft delete object (set its ``is_removed`` field to True) """ self.is_removed = True self.save()
class Bank(TimeStampedModel, SoftDeletableModel): ACCOUNT_TYPE = Choices(('Savings', _('Savings')), ('Check', _('Check'))) uuid = ShortUUIDField(max_length=8, unique=True, editable=False, verbose_name='Public identifier') account_name = models.CharField(max_length=256, blank=True) account_holder_name = models.CharField(max_length=256, blank=True) bank_name = models.CharField(max_length=256, blank=True) account_number = models.PositiveIntegerField( unique=True, validators=[MinValueValidator(10000), MaxValueValidator(999999999999)]) account_type = models.CharField(choices=ACCOUNT_TYPE, default=ACCOUNT_TYPE.Savings, max_length=20) bank_branch_code = models.PositiveIntegerField( unique=False, validators=[MinValueValidator(1000), MaxValueValidator(99999999)]) user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='bank', unique=True, on_delete=models.PROTECT) objects = SoftDeletableManager() removed = QueryManager(is_removed=True) class Meta: ordering = ['account_number'] @property def account_reference_id(self): start_index = int(str(self.account_number)[4]) account_reference = "ACC-" + str( self.account_number) + "-" + self.uuid[start_index:start_index + 4] return account_reference.upper() def __str__(self): return '({}) {}'.format(self.account_name.upper(), self.account_number)
class Article(ExtendedModel): SIGNALS_MAP = { 'updated': (search_signals.update_document, core_signals.notify_updated), 'published': (search_signals.update_document, core_signals.notify_published), 'restored': (search_signals.update_document, core_signals.notify_restored), 'removed': (search_signals.remove_document, core_signals.notify_removed), } title = models.CharField(max_length=300, verbose_name=_("Title")) notes = RichTextUploadingField(verbose_name=_("Notes"), null=True) license_old_id = models.CharField(max_length=20, blank=True, null=True, verbose_name=_("License ID")) license = models.ForeignKey('licenses.License', on_delete=models.DO_NOTHING, blank=True, null=True, verbose_name=_("License ID")) author = models.CharField(max_length=50, blank=True, null=True, verbose_name=_("Author")) category = models.ForeignKey(ArticleCategory, on_delete=models.PROTECT, verbose_name=_('Category')) tags = models.ManyToManyField('tags.Tag', db_table='article_tag', blank=True, verbose_name=_("Tags"), related_name='articles', related_query_name="article") datasets = models.ManyToManyField('datasets.Dataset', db_table='article_dataset', verbose_name=_('Datasets'), related_name='articles', related_query_name="article") created_by = models.ForeignKey(User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Created by"), related_name='articles_created') modified_by = models.ForeignKey(User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Modified by"), related_name='articles_modified') @classmethod def accusative_case(cls): return _("acc: Article") def __str__(self): return self.title def published_datasets(self): return self.datasets.filter(status='published') @property def tags_list(self): return [tag.name_translated for tag in self.tags.all()] i18n = TranslationField(fields=("title", "notes")) tracker = FieldTracker() slugify_field = 'title' raw = models.Manager() objects = SoftDeletableManager() deleted = DeletedManager() class Meta: verbose_name = _("Article") verbose_name_plural = _("Articles") db_table = "article" default_manager_name = "objects" indexes = [ GinIndex(fields=["i18n"]), ]
class Organization(ExtendedModel): SIGNALS_MAP = { 'removed': (remove_related_datasets, search_signals.remove_document_with_related, core_signals.notify_removed), } title = models.CharField(max_length=100, verbose_name=_('Title')) description = models.TextField(blank=True, null=True, verbose_name=_('Description')) image = models.ImageField(max_length=254, storage=storages.get_storage('organizations'), upload_to='%Y%m%d', blank=True, null=True, verbose_name=_('Image URL')) postal_code = models.CharField(max_length=6, null=True, verbose_name=_('Postal code')) city = models.CharField(max_length=200, null=True, verbose_name=_("City")) street_type = models.CharField(max_length=50, null=True, verbose_name=_("Street type")) street = models.CharField(max_length=200, null=True, verbose_name=_("Street")) street_number = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Street number")) flat_number = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Flat number")) email = models.CharField(max_length=300, null=True, verbose_name=_("Email")) epuap = models.CharField(max_length=500, null=True, verbose_name=_("EPUAP")) fax = models.CharField(max_length=50, null=True, verbose_name=_("Fax")) fax_internal = models.CharField(max_length=20, null=True, blank=True, verbose_name=_('int.')) institution_type = models.CharField(max_length=50, choices=INSTITUTION_TYPE_CHOICES, default=INSTITUTION_TYPE_CHOICES[1][0], verbose_name=_("Institution type")) regon = models.CharField(max_length=20, null=True, verbose_name=_("REGON")) tel = models.CharField(max_length=50, null=True, verbose_name=_("Phone")) tel_internal = models.CharField(max_length=20, null=True, blank=True, verbose_name=_('int.')) website = models.CharField(max_length=200, null=True, verbose_name=_("Website")) i18n = TranslationField(fields=('title', 'description', 'slug')) created_by = models.ForeignKey(User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Created by"), related_name='organizations_created') modified_by = models.ForeignKey(User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Modified by"), related_name='organizations_modified') def __str__(self): if self.title: return self.title return self.slug def get_url_path(self): if self.id: try: return reverse("admin:applications_application_change", kwargs={"object_id": self.id}) except NoReverseMatch: return "" return "" @property def image_url(self): try: return self.image.url except ValueError: return '' @property def short_description(self): clean_text = "" if self.description: clean_text = ''.join( BeautifulSoup(self.description, "html.parser").stripped_strings) return clean_text @property def api_url(self): return '/institutions/{}'.format(self.id) @property def description_html(self): return format_html(self.description) @property def datasets_count(self): return self.datasets.count() @classmethod def accusative_case(cls): return _("acc: Institution") @property def published_datasets(self): return self.datasets.filter(status='published') @property def address_display(self): city = ' '.join(i.strip() for i in [self.postal_code, self.city] if i) if not city: return None number = '/'.join(i.strip() for i in [self.street_number, self.flat_number] if i) addres_line = city if self.street: street = ' '.join(i.strip() for i in [self.street_type, self.street, number] if i) addres_line = ', '.join(i for i in [addres_line, street] if i) return addres_line @property def phone_display(self): if not self.tel: return None try: p = phonenumbers.parse(self.tel, 'PL') phone = phonenumbers.format_number( p, phonenumbers.PhoneNumberFormat.INTERNATIONAL) except phonenumbers.phonenumberutil.NumberParseException: return None return _(' int. ').join(i.strip() for i in [phone, self.tel_internal] if i) @property def fax_display(self): if not self.fax: return None try: p = phonenumbers.parse(self.fax, 'PL') fax = phonenumbers.format_number( p, phonenumbers.PhoneNumberFormat.INTERNATIONAL) except phonenumbers.phonenumberutil.NumberParseException: return None return _(' int. ').join(i.strip() for i in [fax, self.fax_internal] if i) raw = models.Manager() objects = SoftDeletableManager() deleted = DeletedManager() tracker = FieldTracker() slugify_field = 'title' short_description.fget.short_description = _("Description") class Meta: db_table = "organization" verbose_name = _("Institution") verbose_name_plural = _("Institutions") default_manager_name = "objects" indexes = [ GinIndex(fields=["i18n"]), ]
class Resource(ExtendedModel): SIGNALS_MAP = { 'updated': ( revalidate_resource, search_signals.update_document_with_related, core_signals.notify_updated ), 'published': ( revalidate_resource, search_signals.update_document_with_related, core_signals.notify_published ), 'restored': ( revalidate_resource, search_signals.update_document_with_related, core_signals.notify_restored ), } file = models.FileField(verbose_name=_("File"), storage=storages.get_storage('resources'), upload_to='%Y%m%d', max_length=2000, blank=True, null=True) packed_file = models.FileField(verbose_name=_("Packed file"), storage=storages.get_storage('resources'), upload_to='%Y%m%d', max_length=2000, blank=True, null=True) file_info = models.TextField(blank=True, null=True, editable=False, verbose_name=_("File info")) file_encoding = models.CharField(max_length=150, null=True, blank=True, editable=False, verbose_name=_("File encoding")) link = models.URLField(verbose_name=_('Resource Link'), max_length=2000, blank=True, null=True) title = models.CharField(max_length=500, verbose_name=_("title")) description = models.TextField(blank=True, null=True, verbose_name=_("Description")) position = models.IntegerField(default=1, verbose_name=_("Position")) dataset = models.ForeignKey('datasets.Dataset', on_delete=models.CASCADE, related_name='resources', verbose_name=_('Dataset')) format = models.CharField(max_length=150, blank=True, null=True, verbose_name=_("Format"), choices=supported_formats_choices()) type = models.CharField(max_length=10, choices=RESOURCE_TYPE, default='file', editable=False, verbose_name=_("Type")) openness_score = models.IntegerField(default=0, verbose_name=_("Openness score"), validators=[MinValueValidator(0), MaxValueValidator(5)]) created_by = models.ForeignKey( User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Created by"), related_name='resources_created' ) modified_by = models.ForeignKey( User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Modified by"), related_name='resources_modified' ) link_tasks = models.ManyToManyField('TaskResult', verbose_name=_('Download Tasks'), blank=True, related_name='link_task_resources', ) file_tasks = models.ManyToManyField('TaskResult', verbose_name=_('Download Tasks'), blank=True, related_name='file_task_resources') data_tasks = models.ManyToManyField('TaskResult', verbose_name=_('Download Tasks'), blank=True, related_name='data_task_resources') old_file = models.FileField(verbose_name=_("File"), storage=storages.get_storage('resources'), upload_to='', max_length=2000, blank=True, null=True) old_resource_type = models.TextField(verbose_name=_("Data type"), null=True) old_format = models.CharField(max_length=150, blank=True, null=True, verbose_name=_("Format")) old_customfields = JSONField(blank=True, null=True, verbose_name=_("Customfields")) old_link = models.URLField(verbose_name=_('Resource Link'), max_length=2000, blank=True, null=True) downloads_count = models.PositiveIntegerField(default=0) show_tabular_view = models.BooleanField(verbose_name=_('Tabular view'), default=True) tabular_data_schema = JSONField(null=True, blank=True) data_date = models.DateField(null=True, verbose_name=_("Data date")) verified = models.DateTimeField(blank=True, default=now, verbose_name=_("Update date")) def __str__(self): return self.title @property def media_type(self): return self.type or '' @property def category(self): return self.dataset.category if self.dataset else '' @property def link_is_valid(self): task = self.link_tasks.last() return task.status if task else 'NOT_AVAILABLE' @property def file_is_valid(self): task = self.file_tasks.last() return task.status if task else 'NOT_AVAILABLE' @property def data_is_valid(self): task = self.data_tasks.last() return task.status if task else 'NOT_AVAILABLE' @property def file_url(self): if self.file: _file_url = self.file.url if not self.packed_file else self.packed_file.url return '%s%s' % (settings.API_URL, _file_url) return '' @property def file_size(self): return self.file.size if self.file else None @property def download_url(self): if self.file: return '{}/resources/{}/file'.format(settings.API_URL, self.ident) return '' @property def is_indexable(self): if self.type == 'file' and not self.file_is_valid: return False if self.type in ('api', 'website') and not self.link_is_valid: return False return True @property def tabular_data(self): if not self._tabular_data: if self.format in ('csv', 'tsv', 'xls', 'xlsx', 'ods') and self.file: self._tabular_data = TabularData(self) return self._tabular_data def index_tabular_data(self, force=False): self.tabular_data.validate() return self.tabular_data.index(force=force) def save_file(self, content, filename): dt = self.created.date() if self.created else now().date() subdir = dt.isoformat().replace("-", "") dest_dir = os.path.join(self.file.storage.location, subdir) os.makedirs(dest_dir, exist_ok=True) file_path = os.path.join(dest_dir, filename) with open(file_path, 'wb') as f: f.write(content.read()) return '%s/%s' % (subdir, filename) def revalidate(self, **kwargs): if not self.link or self.link.startswith(settings.BASE_URL): process_resource_file_task.s(self.id, **kwargs).apply_async(countdown=2) else: process_resource_from_url_task.s(self.id, **kwargs).apply_async(countdown=2) @classmethod def accusative_case(cls): return _("acc: Resource") @property def _openness_score(self): if not self.format: return 0 _, content = content_type_from_file_format(self.format.lower()) return OPENNESS_SCORE.get(content, 0) @property def file_size_human_readable(self): file_size = self.file_size or 0 return sizeof_fmt(file_size) @property def title_truncated(self): title = (self.title[:100] + '..') if len(self.title) > 100 else self.title return title _tabular_data = None i18n = TranslationField(fields=("title", "description")) tracker = FieldTracker() slugify_field = 'title' raw = models.Manager() objects = SoftDeletableManager() deleted = DeletedManager() class Meta: verbose_name = _("Resource") verbose_name_plural = _("Resources") db_table = 'resource' default_manager_name = "objects" indexes = [GinIndex(fields=["i18n"]), ]
class BaseWallet(TimeStampedModel, SoftDeletableModel): user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='%(class)s_wallets', on_delete=models.CASCADE) private = models.CharField(max_length=150, unique=True) public = models.CharField(max_length=150, unique=True) address = models.CharField(max_length=150, unique=True) wif = models.CharField(max_length=150, unique=True) invoices = GenericRelation( 'wallets.Invoice', content_type_field='wallet_type', object_id_field='wallet_id', ) payments = GenericRelation( 'wallets.Payment', content_type_field='wallet_type', object_id_field='wallet_id', ) objects = SoftDeletableManager() removed = QueryManager(is_removed=True) class Meta: abstract = True ordering = ['id'] def __str__(self): return '({}) {}'.format(self.coin_symbol.upper(), self.address) def get_absolute_url(self): return reverse('wallets:detail', kwargs={ 'wallet': self.coin_symbol, 'address': self.address }) @cached_property def coin_symbol(self): coin_symbol = self.__class__.get_coin_symbol() return coin_symbol @cached_property def coin_name(self): coin_name = self.__class__.get_coin_name() return coin_name def spend(self, addresses: List[str], amounts: List[int]) -> str: assert len(addresses) == len(amounts), ( 'The number of addresses and amounts should be the same') new_transaction = api.not_simple_spend(from_privkey=self.private, to_addresses=addresses, to_satoshis=amounts, coin_symbol=self.coin_symbol, api_key=get_api_key()) return new_transaction def spend_with_webhook(self, addresses: List[str], amounts: List[int], invoice: object = None, obj: object = None, event: str = 'tx-confirmation') -> str: assert len(addresses) == len(amounts), ( 'The number of addresses and amounts should be the same') new_transaction = api.not_simple_spend(from_privkey=self.private, to_addresses=addresses, to_satoshis=amounts, coin_symbol=self.coin_symbol, api_key=get_api_key()) self.set_webhook(to_addresses=addresses, transaction=new_transaction, obj=obj, invoice=invoice, event=event) return new_transaction def set_webhook(self, to_addresses: List[str], transaction: str, obj: object = None, invoice: object = None, event: str = 'tx-confirmation') -> str: domain = settings.DOMAIN_NAME if obj: try: obj = signing.dumps({ 'app_label': obj._meta.app_label, 'model': obj._meta.model_name, 'id': obj.id }) except Exception: obj = None signature = signing.dumps({ 'from_address': self.address, 'to_addresses': to_addresses, 'symbol': self.coin_symbol, 'event': event, 'transaction_id': transaction, 'invoice_id': invoice.id if invoice else None, 'content_object': obj }) callback_url = 'https://{}/wallets/webhook/{}/'.format( domain, signature) webhook = blockcypher.subscribe_to_address_webhook( callback_url=callback_url, subscription_address=self.address, event=event, coin_symbol=self.coin_symbol, api_key=get_api_key() # settings.BLOCKCYPHER_API_KEY ) return webhook @ecached_property('address_details:{self.id}', 60) def address_details(self): details = blockcypher.get_address_details(self.address, coin_symbol=self.coin_symbol) return details @ecached_property('overview:{self.id}', 60) def overview(self): overview = blockcypher.get_address_overview( self.address, coin_symbol=self.coin_symbol) return overview @ecached_property('balance:{self.id}', 60) def balance(self): overview = blockcypher.get_address_overview( self.address, coin_symbol=self.coin_symbol) return overview['balance'] @ecached_property('natural_balance:{self.id}', 60) def normal_balance(self): return from_satoshi(self.balance) @ecached_property('usd_balance:{self.id}', 60) def usd_balance(self): return round((self.normal_balance * self.__class__.get_rate()), 2) @ecached_property('transactions:{self.id}', 60) def transactions(self): get_address_full = self.address_details return get_address_full['txrefs'] @staticmethod def transaction_details(tx_ref: str, coin_symbol='btc') -> dict: details = blockcypher.get_transaction_details(tx_ref, coin_symbol) return details def create_invoice(self, content_object: object, data: list) -> object: key_list = ['wallet', 'amount', 'content_object', 'purpose'] invoice = Invoice.objects.create(wallet=self, content_object=content_object) assign_perm('pay_invoice', self.user, invoice) assign_perm('view_invoice', self.user, invoice) for item in data: if all(key in item for key in key_list): payment = Payment(invoice=invoice, amount=item['amount'], wallet=item['wallet'], content_object=item['content_object'], purpose=item['purpose']) payment.save() assign_perm('view_payment', self.user, payment) assign_perm('view_payment', item['wallet'].user, payment) return invoice @classmethod def _get_coin_symbol_and_name(cls): if cls.__name__.lower().startswith('btc'): coin_symbol = 'btc' coin_name = 'bitcoin' elif cls.__name__.lower().startswith('ltc'): coin_symbol = 'ltc' coin_name = 'litecoin' elif cls.__name__.lower().startswith('dash'): coin_symbol = 'dash' coin_name = 'dash' elif cls.__name__.lower().startswith('doge'): coin_symbol = 'doge' coin_name = 'dogecoin' elif cls.__name__.lower().startswith('bcy'): coin_symbol = 'bcy' coin_name = 'blockcypher' return {'coin_symbol': coin_symbol, 'coin_name': coin_name} @classmethod def get_coin_symbol(cls): return cls._get_coin_symbol_and_name()['coin_symbol'] @classmethod def get_coin_name(cls): return cls._get_coin_symbol_and_name()['coin_name'] @classmethod def get_rate(cls): coin_name = cls.get_coin_name() if coin_name == 'blockcypher': return 150 # for tests and debug try: response = requests.get( 'https://api.coinmarketcap.com/v1/ticker/{}/'.format( coin_name)) json_response = response.json() return float(json_response[0]['price_usd']) except Exception: return 0
class Application(ExtendedModel): SIGNALS_MAP = { 'updated': (generate_thumbnail, core_signals.notify_updated), 'published': (generate_thumbnail, core_signals.notify_published), 'restored': (generate_thumbnail, core_signals.notify_restored), 'removed': (search_signals.remove_document, core_signals.notify_removed), 'm2m_added': (search_signals.update_document, core_signals.notify_m2m_added,), 'm2m_removed': (search_signals.update_document, core_signals.notify_m2m_removed,), 'm2m_cleaned': (search_signals.update_document, core_signals.notify_m2m_cleaned,), } title = models.CharField(max_length=300, verbose_name=_("Title")) notes = models.TextField(verbose_name=_("Notes"), null=True) author = models.CharField(max_length=50, blank=True, null=True, verbose_name=_("Author")) url = models.URLField(max_length=300, verbose_name=_("App URL"), null=True) image = models.ImageField( max_length=200, storage=storages.get_storage('applications'), upload_to='%Y%m%d', blank=True, null=True, verbose_name=_("Image URL") ) image_thumb = models.ImageField( storage=storages.get_storage('applications'), upload_to='%Y%m%d', blank=True, null=True ) datasets = models.ManyToManyField('datasets.Dataset', db_table='application_dataset', verbose_name=_('Datasets'), related_name='applications', related_query_name="application") tags = models.ManyToManyField('tags.Tag', blank=True, db_table='application_tag', verbose_name=_('Tag'), related_name='applications', related_query_name="application") created_by = models.ForeignKey( User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Created by"), related_name='applications_created' ) modified_by = models.ForeignKey( User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Modified by"), related_name='applications_modified' ) @cached_property def image_url(self): try: return self.image.url except ValueError: return '' @cached_property def image_thumb_url(self): try: return self.image_thumb.url except ValueError: return '' @cached_property def tags_list(self): return [tag.name_translated for tag in self.tags.all()] @cached_property def users_following_list(self): return [user.id for user in self.users_following.all()] def __str__(self): return self.title @classmethod def accusative_case(cls): return _("acc: Application") def published_datasets(self): return self.datasets.filter(status='published') i18n = TranslationField(fields=("title", "notes")) raw = models.Manager() objects = SoftDeletableManager() deleted = DeletedManager() tracker = FieldTracker() slugify_field = 'title' class Meta: verbose_name = _("Application") verbose_name_plural = _("Applications") db_table = "application" default_manager_name = "objects" indexes = [GinIndex(fields=["i18n"]), ]
class Category(ExtendedModel): SIGNALS_MAP = { 'updated': (update_related_datasets, ), 'published': (update_related_datasets, ), 'restored': (update_related_datasets, ), 'removed': (null_in_related_datasets, ), } title = models.CharField(max_length=100, verbose_name=_("Title")) description = models.TextField(null=True, verbose_name=_("Description")) color = models.CharField(max_length=20, default="#000000", null=True, verbose_name=_("Color")) image = models.ImageField(max_length=200, storage=storages.get_storage('common'), upload_to='', blank=True, null=True, verbose_name=_("Image URL")) created_by = models.ForeignKey(User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Created by"), related_name='categories_created') modified_by = models.ForeignKey(User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Modified by"), related_name='categories_modified') @classmethod def accusative_case(cls): return _("acc: Category") def __str__(self): return self.title_i18n @property def image_url(self): if not self.image or not self.image.url: return None return '{}{}'.format(settings.BASE_URL, self.image.url) i18n = TranslationField(fields=("title", "description")) raw = models.Manager() objects = SoftDeletableManager() deleted = DeletedManager() tracker = FieldTracker() slugify_field = 'title' class Meta: db_table = "category" verbose_name = _("Category") verbose_name_plural = _("Categories") default_manager_name = "objects" indexes = [ GinIndex(fields=["i18n"]), ]
class Tag(ExtendedModel): SIGNALS_MAP = { 'updated': (update_related_datasets, update_related_articles, update_related_applications ), 'published': (update_related_datasets, update_related_articles, update_related_applications ), 'restored': (update_related_datasets, update_related_articles, update_related_applications ), 'removed': (update_related_datasets, update_related_articles, update_related_applications ), } name = models.CharField(unique=True, max_length=100, verbose_name=_("name")) created_by = models.ForeignKey( User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Created by"), related_name='tags_created' ) modified_by = models.ForeignKey( User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Modified by"), related_name='tags_modified' ) def __str__(self): return self.name @classmethod def accusative_case(cls): return _("acc: Tag") i18n = TranslationField(fields=("name",), required_languages=("pl",)) tracker = FieldTracker() slugify_field = 'name' raw = models.Manager() objects = SoftDeletableManager() deleted = DeletedManager() class Meta: verbose_name = _("Tag") verbose_name_plural = _("Tags") db_table = 'tag' default_manager_name = "objects" indexes = [GinIndex(fields=["i18n"]), ]
class Invoice(TimeStampedModel, SoftDeletableModel): limit = models.Q(app_label='wallets', model='btc') | \ models.Q(app_label='wallets', model='ltc') | \ models.Q(app_label='wallets', model='dash') | \ models.Q(app_label='wallets', model='doge') | \ models.Q(app_label='wallets', model='bcy') wallet_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, limit_choices_to=limit, related_name='invoices') wallet_id = models.PositiveIntegerField() wallet = GenericForeignKey('wallet_type', 'wallet_id') content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True) object_id = models.PositiveIntegerField(null=True, blank=True) content_object = GenericForeignKey('content_type', 'object_id') tx_ref = models.CharField(max_length=100, null=True, blank=True) is_paid = models.BooleanField(default=False) expires = models.DateTimeField(default=get_expires_date) objects = SoftDeletableManager() removed = QueryManager(is_removed=True) class Meta: ordering = ['id'] permissions = ( ('view_invoice', _('Can view invoice')), ('pay_invoice', _('Can pay invoice')), ) def __init__(self, *args, **kwargs): super(Invoice, self).__init__(*args, **kwargs) self.__tracked_fields = ['is_paid'] self.set_original_values() def save(self, *args, **kwargs): self.full_clean() if self.has_changed(): if not self.can_be_paid(): self.reset_original_values() self.set_original_values() return super(Invoice, self).save(*args, **kwargs) def set_original_values(self): for field in self.__tracked_fields: if getattr(self, field) is 'True': setattr(self, '__original_%s' % field, True) elif getattr(self, field) is 'False': setattr(self, '__original_%s' % field, False) else: setattr(self, '__original_%s' % field, getattr(self, field)) return self.__dict__ def has_changed(self): for field in self.__tracked_fields: original = '__original_%s' % field return getattr(self, original) != getattr(self, field) def reset_original_values(self): values = {} for field in self.__tracked_fields: original = '__original_%s' % field setattr(self, field, getattr(self, original)) values[field] = getattr(self, field) return values @property def amount(self): return sum([payment.amount for payment in self.payments.all()]) @property def normal_amount(self): return format(from_satoshi(self.amount), '.8f') @property def usd_amount(self): return round((self.normal_amount * self.wallet.get_rate()), 2) def pay(self): if self.wallet.user.has_perm('pay_invoice', self) \ and not self.is_expired: payments = self.payments.all() data = [{ 'address': payment.wallet.address, 'amount': payment.amount } for payment in payments] tx_ref = self.wallet.spend( addresses=[payment['address'] for payment in data], amounts=[payment['amount'] for payment in data], #invoice=self, #obj=self.content_object ) self.tx_ref = tx_ref self.save() return tx_ref return None def get_absolute_url(self): return reverse('wallets:invoice_detail', kwargs={'pk': self.pk}) @property def is_expired(self): return timezone.now() > self.expires def can_be_paid(self): if self.is_expired: return False details = self.wallet.transaction_details(self.tx_ref, self.wallet.coin_symbol) if self.amount < details['outputs'][0]['value']: logger.error( 'Invoice can be confirmed, because the amount of all ' + 'transactions is less than the amount of the invoice.') return False return True @ecached_property('normal_amount:{self.id}', 60 * 5) def normal_amount(self): return from_satoshi(self.amount)
class Payment(TimeStampedModel, SoftDeletableModel): limit = models.Q(app_label='wallets', model='btc') | \ models.Q(app_label='wallets', model='ltc') | \ models.Q(app_label='wallets', model='dash') | \ models.Q(app_label='wallets', model='doge') | \ models.Q(app_label='wallets', model='bcy') invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name='payments', null=False, blank=True) amount = models.BigIntegerField(validators=[MinValueValidator(0)]) wallet_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, limit_choices_to=limit, related_name='payments') wallet_id = models.PositiveIntegerField() wallet = GenericForeignKey('wallet_type', 'wallet_id') content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True) object_id = models.PositiveIntegerField(null=True, blank=True) content_object = GenericForeignKey('content_type', 'object_id') purpose = models.CharField(max_length=255) objects = SoftDeletableManager() removed = QueryManager(is_removed=True) class Meta: ordering = ['id'] permissions = (('view_payment', _('Can view payment')), ) @property def normal_amount(self): return format(from_satoshi(self.amount), '.8f') @property def usd_amount(self): return round((float(self.normal_amount) * self.wallet.get_rate()), 2) @property def text(self): return 'User {user} paid {amount} ($ {usd_amount}) \ {symbol} for {purpose}'.format( user=self.invoice.wallet.user, amount=self.amount, usd_amount=self.usd_amount, symbol=self.invoice.wallet.coin_symbol.upper(), purpose=self.purpose, #obj=self.content_object.__str__() ) @property def html(self): try: html = format_html( 'User <a href="{}">{}</a> paid {} {} for' + ' {} <a href="{}">({})</a>', self.invoice.wallet.user.get_absolute_url(), self.invoice.wallet.user, self.amount, self.invoice.wallet.coin_symbol.upper(), self.purpose, self.content_object.get_absolute_url(), self.content_object.__str__()) return html except: return ''
class Wallet(TimeStampedModel, SoftDeletableModel): bank = models.OneToOneField('wallet.Bank', related_name='wallet', unique=True, on_delete=models.PROTECT) objects = SoftDeletableManager() removed = QueryManager(is_removed=True) class Meta: ordering = ['bank'] permissions = (('can_view_wallet_report', 'Can view wallet report'), ) @staticmethod def validate_amount(amount): if not isinstance(amount, (int, Decimal, str)): raise ValueError('Amount need to be a string, integer or Decimal.') return Decimal(amount) def register_income(self, amount, reference): amount = self.validate_amount(amount) if amount <= Decimal('0'): raise ValueError('Amount need to be positive number, not %s' % amount) return self._create_transaction(value, reference) def _create_transaction(self, amount, reference): return Transaction.objects.create(wallet=self, reference=reference, amount=amount) def register_expense(self, amount, reference, current_lock=None): amount = self.validate_amount(amount) if amount >= Decimal('0'): raise ValueError('Amount need to be negative number, not %s' % amount) if current_lock is not None: if current_lock.wallet != self: raise ValueError('Lock not for this wallet!') if self.balance >= amount: return self._create_transaction(amount, reference) else: raise WalletHasInsufficientFunds( 'Not insufficient funds to spend %s' % amount) with Locked(self): if self.balance >= amount: return self._create_transaction(amount, reference) else: raise WalletHasInsufficientFunds( 'Not insufficient funds to spend %s' % amount) @property def balance(self): return self.transactions.aggregate( Sum('transaction_amount'))['transaction_amount__sum'] def __str__(self): return '({})'.format(self.bank)
class BaseModel(SoftDeletableModel): user = models.ForeignKey( User, verbose_name="User", on_delete=models.CASCADE, unique=False ) uuid = UUIDField( default=uuid.uuid4, editable=False, unique=True ) created_at = models.DateTimeField( auto_now_add=True, verbose_name="Created at" ) updated_at = models.DateTimeField( auto_now=True, verbose_name="Updated at" ) deleted_at = models.DateTimeField( null=True, blank=True, verbose_name="Deleted at" ) created_by = models.ForeignKey( User, null=True, verbose_name="Created by", related_name='%(class)s_created_by', on_delete=models.CASCADE ) updated_by = models.ForeignKey( User, null=True, verbose_name="Updated by", related_name='%(class)s_updated_by', on_delete=models.CASCADE ) deleted_by = models.ForeignKey( User, null=True, blank=True, verbose_name="Deleted by", related_name='%(class)s_deleted_by', on_delete=models.CASCADE ) objects = SoftDeletableManager() def __str__(self): if hasattr(self, 'name'): return "%s" % self.name if hasattr(self, 'description'): return "%s" % self.description if self._meta.verbose_name: return "%s" % self._meta.verbose_name return self.pk @classmethod def list(cls, *args, **kwargs): return cls.objects.filter(*args, **kwargs) @classmethod def count(cls, *args, **kwargs): return cls.list(*args, **kwargs).count() @classmethod def create(cls, **kwargs): cls.objects.create(**kwargs) @classmethod def get(cls, *args, **kwargs): return cls.list(*args, **kwargs).first() @classmethod def choices(cls): choices = () for model in cls.objects.all(): choice = (model.pk, model.name if hasattr(model, 'name') else model.pk) choices += choice return choices @classmethod def retrieve(cls, **kwargs): return cls.list(**kwargs).first() class Meta: abstract = True
class Dataset(ExtendedModel): SIGNALS_MAP = { 'removed': (remove_related_resources, search_signals.remove_document_with_related, core_signals.notify_removed), } title = models.CharField(max_length=300, null=True, verbose_name=_("Title")) version = models.CharField(max_length=100, blank=True, null=True, verbose_name=_("Version")) url = models.CharField(max_length=1000, blank=True, null=True, verbose_name=_("Url")) notes = models.TextField(verbose_name=_("Notes"), null=True, blank=False) license_old_id = models.CharField(max_length=20, blank=True, null=True, verbose_name=_("License ID")) license = models.ForeignKey('licenses.License', on_delete=models.DO_NOTHING, blank=True, null=True, verbose_name=_("License ID")) license_condition_db_or_copyrighted = models.CharField(max_length=300, blank=True, null=True) license_condition_modification = models.NullBooleanField(null=True, blank=True, default=None) license_condition_original = models.NullBooleanField(null=True, blank=True, default=None) license_condition_responsibilities = models.TextField(blank=True, null=True) license_condition_source = models.NullBooleanField(null=True, blank=True, default=None) license_condition_timestamp = models.NullBooleanField(null=True, blank=True) organization = models.ForeignKey('organizations.Organization', on_delete=models.CASCADE, related_name='datasets', verbose_name=_('Institution')) customfields = JSONField(blank=True, null=True, verbose_name=_("Customfields")) update_frequency = models.CharField(max_length=50, blank=True, null=True, verbose_name=_("Update frequency")) category = models.ForeignKey('categories.Category', on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('Category')) tags = models.ManyToManyField('tags.Tag', db_table='dataset_tag', blank=False, verbose_name=_("Tag"), related_name='datasets', related_query_name="dataset") created_by = models.ForeignKey(User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Created by"), related_name='datasets_created') modified_by = models.ForeignKey(User, models.DO_NOTHING, blank=False, editable=False, null=True, verbose_name=_("Modified by"), related_name='datasets_modified') verified = models.DateTimeField(blank=True, default=now, verbose_name=_("Update date")) def __str__(self): return self.title @property def institution(self): return self.organization @property def downloads_count(self): return sum(res.downloads_count for res in self.resources.all()) @property def formats(self): return list( set(res.format for res in self.resources.all() if res.format is not None)) @property def openness_scores(self): return list(set(res.openness_score for res in self.resources.all())) @property def tags_list(self): return [tag.name_translated for tag in self.tags.all()] @property def license_name(self): return self.license.name if self.license and self.license.name else '' @property def license_description(self): return self.license.title if self.license and self.license.title else '' @property def last_modified_resource(self): return self.resources.all().aggregate(Max('modified'))['modified__max'] last_modified_resource.fget.short_description = _("modified") @property def is_license_set(self): return any([ self.license, self.license_condition_db_or_copyrighted, self.license_condition_modification, self.license_condition_original, self.license_condition_responsibilities ]) @property def followers_count(self): return self.users_following.count() @classmethod def accusative_case(cls): return _("acc: Dataset") i18n = TranslationField(fields=("title", "notes")) raw = models.Manager() objects = SoftDeletableManager() deleted = DeletedManager() tracker = FieldTracker() slugify_field = 'title' last_modified_resource.fget.short_description = _("modified") class Meta: verbose_name = _("Dataset") verbose_name_plural = _("Datasets") db_table = 'dataset' default_manager_name = "objects" indexes = [ GinIndex(fields=["i18n"]), ]
class MobileApp(SoftDeletableModel, OrderedModel, CommonActionModel): name = models.CharField(max_length=100,unique=True, db_index=True, verbose_name=_('App Name')) description = models.TextField(blank=True, null=True, verbose_name=_('Description')) upload_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_('Uploader')) upload_date = models.DateTimeField(auto_now_add=True, verbose_name=_('Upload Time'), db_index=True) category = models.ForeignKey(AppCategory, on_delete=models.CASCADE) comment_count = models.PositiveIntegerField(null=True, verbose_name=_('Comment Count'), default=0) download_count = models.PositiveIntegerField(null=True, verbose_name=_('Download Count'), default=0) images = GenericRelation(Image, related_query_name='imaged_app', verbose_name=_('Screenshots')) is_active = models.BooleanField(default=False, verbose_name=_('Active Status')) icon = models.ImageField(upload_to="icons") developer = models.CharField(max_length=100, blank=True, null=True, verbose_name=_("Developer")) videos = GenericRelation(Video, related_query_name='video_app', verbose_name=_('Video Show')) order_with_respect_to = 'category' objects = models.Manager() exist_objects = SoftDeletableManager() active_apps = ActiveAppManager() shown_apps = ShownAppManager() upload_order = UploadOrderManager() class Meta(OrderedModel.Meta): permissions = (("can_approve_app", "Can approve newly uploaded app"),) def __str__(self): return self.name def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name, allow_unicode=True) super(MobileApp, self).save(*args, **kwargs) def latest_version(self): if self.canShow(): return self.versions.filter(approve_status='approved').select_related('apk')[0] return None def get_absolute_url(self): return reverse('showcase:app', args=[self.slug]) def has_approved_version(self): if self.versions.filter(approve_status='approved').exists(): return True return False def canShow(self): return self.is_active and self.has_approved_version() @property def userLink(self): return reverse('showcase:app', args=[self.slug]) @property def user_link_mobile(self): return reverse('mobile:app', args=[self.slug]) @property def latestAPK(self): latestVersion = self.latest_version() if latestVersion is not None: return latestVersion.apk.file.url return None def latestTime(self): latestVersion = self.latest_version() if latestVersion is not None: return latestVersion.created_time return None @property def avg_rate(self): last_rating = self.ratings.last() if last_rating is not None: return last_rating.average return 5.0