class Category(models.Model): name = models.CharField(max_length=100) top_level = models.BooleanField(default=False) slug = AutoSlugField(populate_from='name', max_length=100, blank=True, null=True) order_by = models.IntegerField(default=0) categories = CategoryQuerySet.as_manager() objects = CategoryQuerySet.as_manager() class Meta: verbose_name = _('Category') verbose_name_plural = _('Categories') ordering = ('name', ) def __unicode__(self): return self.name @classmethod def api_translation_fields(cls): """Returns name field's translation field names""" return [ f.name for f in cls._meta.get_fields() if f.name.startswith('name') and f.name != 'name' ]
class Collection(TimestampBase): PUBLIC = 'public' PRIVATE = 'private' VISIBILITY_TYPES = ( (PUBLIC, _('Public')), (PRIVATE, _('Private')), ) title = models.TextField(blank=False, null=False, help_text=_("A title for the collection")) description = models.TextField(blank=True, null=True, default=None, help_text=_("A description of the collection")) visibility = models.CharField( max_length=50, choices=VISIBILITY_TYPES, default=PRIVATE) image = models.ImageField( upload_to='collection/%Y/%m/%d', null=True, blank=True) slug = AutoSlugField(populate_from='title', max_length=255, blank=True, null=True) class Meta: verbose_name = _('Collection') verbose_name_plural = _('Collections') ordering = ('title',) def __str__(self): return self.title def get_absolute_url(self): return reverse('orb_collection', args=[self.slug]) def image_filename(self): return os.path.basename(self.image.name)
class Tag(TimestampBase): category = models.ForeignKey(Category) parent_tag = models.ForeignKey('self', blank=True, null=True, default=None, related_name="children") name = models.CharField(max_length=100) create_user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='tag_create_user', blank=True, null=True, default=None, on_delete=models.SET_NULL) update_user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='tag_update_user', blank=True, null=True, default=None, on_delete=models.SET_NULL) image = models.ImageField(upload_to='tag/%Y/%m/%d', null=True, blank=True) slug = AutoSlugField(populate_from='name', max_length=100, blank=True, null=True) order_by = models.IntegerField(default=0) external_url = models.URLField(blank=True, null=True, default=None, max_length=500) description = models.TextField(blank=True, null=True, default=None) summary = models.CharField(blank=True, null=True, max_length=100) contact_email = models.CharField(blank=True, null=True, max_length=100) published = models.BooleanField( default=True, help_text=_(u"Used to toggle status of health domains.")) tags = TagQuerySet.as_manager() objects = TagQuerySet.as_manager() # objects = tags # backwards compatibility class Meta: verbose_name = _('Tag') verbose_name_plural = _('Tags') ordering = ('name', ) unique_together = ('name', 'category') def __unicode__(self): return self.name def get_absolute_url(self): return urlresolvers.reverse('orb_tags', args=[self.slug]) def save(self, *args, **kwargs): if self.image and (self.image.name.startswith("http://") or self.image.name.startswith("https://")): remote_image_file = self.image.name else: remote_image_file = None # add generic geography icon if not specified if self.category.slug == 'geography' and not self.image: self.image = 'tag/geography_default.png' # add generic language icon if not specified if self.category.slug == 'language' and not self.image: self.image = 'tag/language_default.png' # add generic organization icon if not specified if self.category.slug == 'organisation' and not self.image: self.image = 'tag/organisation_default.png' # add generic other icon if not specified if self.category.slug == 'other' and not self.image: self.image = 'tag/other_default.png' super(Tag, self).save(*args, **kwargs) if remote_image_file: image_cleaner(self, url=remote_image_file) return self def image_filename(self): return os.path.basename(self.image.name) def get_property(self, name): props = TagProperty.objects.filter(tag=self, name=name) return props def merge(self, other): """ Merges another tag into this one In doing so it 'merges' relations by ensuring tagged resources are maintained Args: other: the 'loser' Tag instance Returns: None """ # The following is a more sensible query-based way of updating, however # due to a limitation in MySQL this is not possible (MySQL Error 1093): # >>> other.resourcetag.exclude(resource__resourcetag__tag=self).update(tag=self) for resource_tag in other.resourcetag.exclude( resource__resourcetag__tag=self): resource_tag.tag = self resource_tag.save() other.tracker.all().update(tag=self) other.delete()
class Resource(TimestampBase): REJECTED = 'rejected' APPROVED = 'approved' PENDING = 'pending' ARCHIVED = 'archived' STATUS_TYPES = ( (APPROVED, _('Approved')), (PENDING, _('Pending')), (REJECTED, _('Rejected')), (ARCHIVED, _('Archived')), ) MINS = 'mins' HOURS = 'hours' DAYS = 'days' WEEKS = 'weeks' STUDY_TIME_UNITS = ( (MINS, _('Mins')), (HOURS, _('Hours')), (DAYS, _('Days')), (WEEKS, _('Weeks')), ) guid = models.UUIDField(null=True, default=uuid.uuid4, unique=True, editable=False) title = models.TextField(blank=False, null=False) description = models.TextField(blank=False, null=False) image = models.ImageField(upload_to='resourceimage/%Y/%m/%d', max_length=200, blank=True, null=True) status = models.CharField(max_length=50, choices=STATUS_TYPES, default=PENDING) create_user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='resource_create_user', blank=True, null=True, default=None, on_delete=models.SET_NULL) update_user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='resource_update_user', blank=True, null=True, default=None, on_delete=models.SET_NULL) slug = AutoSlugField(populate_from='title', max_length=100, blank=True, null=True) study_time_number = models.IntegerField(default=0, null=True, blank=True) study_time_unit = models.CharField(max_length=10, choices=STUDY_TIME_UNITS, blank=True, null=True) born_on = models.DateTimeField(blank=True, null=True, default=None) attribution = models.TextField(blank=True, null=True, default=None) # Tracking fields source_url = models.URLField(null=True, blank=True, help_text=_(u"Original resource URL.")) source_name = models.CharField( null=True, blank=True, max_length=200, help_text=_( u"Name of the source ORB instance where resource was sourced.")) source_host = models.URLField( null=True, blank=True, help_text=_( u"Host URL of the original ORB instance where resource was sourced." )) source_peer = models.ForeignKey( 'peers.Peer', null=True, blank=True, related_name="resources", help_text=_(u"The peer ORB from which the resource was downloaded.")) tags = models.ManyToManyField('Tag', through='ResourceTag', blank=True) resources = ResourceQueryset.as_manager() objects = ResourceQueryset.as_manager() # objects = resources # alias # Fields to strip from API data API_EXCLUDED_FIELDS = ['id', 'guid'] class Meta: verbose_name = _(u'Resource') verbose_name_plural = _(u'Resources') ordering = ('title', ) def __unicode__(self): return self.title def save(self, **kwargs): """Cleans API submitted images""" if self.image and (self.image.name.startswith("http://") or self.image.name.startswith("https://")): remote_image_file = self.image.name else: remote_image_file = None super(Resource, self).save(**kwargs) if remote_image_file: image_cleaner(self, url=remote_image_file) return self def get_absolute_url(self): return urlresolvers.reverse('orb_resource', args=[self.slug]) def update_from_api(self, api_data): """ Conditionally updates a resource based on API data. Args: api_data: serialized data from the ORB API describing a resource in detail Returns: boolean for whether the resource needed updating """ if self.is_local(): raise LookupError( "Cannot update a locally created resource from API data") if api_data['guid'] != str(self.guid): raise LookupError( "API GUID {} does not match local GUID {}".format( api_data['guid'], str(self.guid))) updated_time, result = cal.parseDT(api_data.pop('update_date')) created_time, result = cal.parseDT(api_data.pop('create_date')) if updated_time.date <= self.create_date.date: return False resource_files = api_data.pop('files', []) languages = api_data.pop('languages', []) tags = api_data.pop('tags', []) resource_urls = api_data.pop('urls', []) resource_uri = api_data.pop('resource_uri') url = api_data.pop('url') for field in self.API_EXCLUDED_FIELDS: api_data.pop(field, None) import_user = get_import_user() cleaned_api_data = clean_api_data(api_data, 'attribution', 'description', 'title') for attr, value in cleaned_api_data.iteritems(): setattr(self, attr, value) self.update_user = import_user self.save() return True @classmethod def create_from_api(cls, api_data, peer=None): """ Creates a new Resource object and its suite of related content based on a dictionary of data as returned from the ORB API Args: api_data: serialized data from the ORB API describing a resource in detail Returns: the Resource object created """ resource_files = api_data.pop('files', []) languages = api_data.pop('languages', []) tags = api_data.pop('tags', []) resource_urls = api_data.pop('urls', []) resource_uri = api_data.pop('resource_uri') url = api_data.pop('url') import_user = get_import_user() cleaned_api_data = clean_api_data(api_data, 'attribution', 'description', 'title') resource = cls.resources.create(source_peer=peer, create_user=import_user, update_user=import_user, **cleaned_api_data) ResourceURL.objects.bulk_create([ ResourceURL.from_url_data( resource, clean_api_data(resource_url_data, "description", "title"), import_user, ) for resource_url_data in resource_urls ] + [ ResourceURL.from_file_data( resource, clean_api_data(resource_file_data, "description", "title"), import_user, ) for resource_file_data in resource_files ]) for tag_data in tags: ResourceTag.create_from_api_data( resource, clean_api_data(tag_data['tag'], "category", "description", "name", "summary"), user=import_user, ) return resource def approve(self): self.status = self.APPROVED self.content_reviews.all().update(status=self.APPROVED) signals.resource_approved.send(sender=self, resource=self) def reject(self): self.status = self.REJECTED self.content_reviews.all().update(status=self.REJECTED) signals.resource_rejected.send(sender=self, resource=self) def is_pending(self): return self.status not in [self.REJECTED, self.APPROVED] def is_local(self): return not bool(self.source_peer) def has_assignments(self): """Returns whether there are *any* reivew assignments""" return self.content_reviews.all().exists() def get_organisations(self): return Tag.objects.filter(resourcetag__resource=self, category__slug='organisation') def get_files(self): return ResourceFile.objects.filter(resource=self).order_by('order_by') def get_urls(self): return ResourceURL.objects.filter(resource=self).order_by('order_by') def get_categories(self): categories = Category.objects.filter( tag__resourcetag__resource=self).distinct().order_by('order_by') for c in categories: c.tags = Tag.objects.filter(resourcetag__resource=self, category=c) return categories def get_display_categories(self): categories = Category.objects.filter( tag__resourcetag__resource=self).exclude( slug='license').distinct().order_by('order_by') for c in categories: c.tags = Tag.tags.filter(category=c).by_resource(self) return categories def get_category(self, category_slug): tags = Tag.objects.filter(resourcetag__resource=self, category__slug=category_slug) return tags def get_type_tags(self): tags = Tag.objects.filter(resourcetag__resource=self, category__slug='type') return tags def get_no_hits(self): anon = ResourceTracker.objects.filter( resource=self, user=None).values_list('ip', flat=True).distinct().count() identified = ResourceTracker.objects.filter(resource=self).exclude( user=None).values_list('user', flat=True).distinct().count() return anon + identified def get_geographies(self): return Tag.tags.by_category('geography').by_resource(self) def get_devices(self): return Tag.tags.by_category('device').by_resource(self) def get_languages(self): return Tag.tags.by_category('language').by_resource(self) def get_license(self): # type: () -> Optional[Tag] """Returns the license tag or None There is expected to be one license for a resource but because there is no hard data restriction on this we check for the first match. """ return Tag.tags.by_category('license').by_resource(self).first() def get_health_domains(self): return Tag.tags.by_category('health-domain').by_resource(self) def get_rating(self): rating = ResourceRating.objects.filter(resource=self).aggregate( rating=Avg('rating'), count=Count('rating')) if rating['rating']: rating['rating'] = round(rating['rating'], 0) return rating def available_languages(self): """ Returns a list of site languages for which this resource has translations This is based on having both title and description for these fields. """ field_names = { language[0]: [ build_localized_fieldname(field, language[0]) for field in ["title", "description"] ] for language in settings.LANGUAGES } return [ language for language, fields in field_names.items() if all([getattr(self, field) for field in fields]) ] def user_can_view(self, user): if self.status == Resource.APPROVED: return True elif user.is_anonymous(): return False elif ((user.is_staff or user == self.create_user or user == self.update_user) or (user.userprofile and (user.userprofile.is_reviewer))): return True else: return False