class Article(PageBase): """A news article.""" objects = ArticleManager() news_feed = models.ForeignKey( NewsFeed, default=get_default_news_feed, ) date = models.DateField( db_index=True, default=timezone.now, ) image = ImageRefField( blank=True, null=True, ) content = HtmlField(blank=True, ) summary = HtmlField(blank=True, ) categories = models.ManyToManyField( Category, blank=True, ) authors = models.ManyToManyField( User, blank=True, ) def _get_permalink_for_page(self, page): """Returns the URL of this article for the given news feed page.""" return page.reverse("article_detail", kwargs={ "year": self.date.year, "month": self.date.strftime("%b").lower(), "day": self.date.day, "url_title": self.url_title, }) def get_absolute_url(self): """Returns the URL of the article.""" return self._get_permalink_for_page(self.news_feed.page) class Meta: unique_together = (( "news_feed", "date", "url_title", ), ) ordering = ("-date", )
class ContentSection(models.Model): # This is a model which will be registered inline so that you can edit it # directly from the page's admin screen. # # This ForeignKey to `pages.Page` is entirely necessary in order to make # inlines work. Note that it is to the *Page* itself and not the content # model! page = models.ForeignKey( 'pages.Page', on_delete=models.CASCADE, ) title = models.CharField( max_length=100, ) # HtmlField is explained in more depth over in the news app. text = HtmlField( null=True, blank=True, ) order = models.PositiveIntegerField( default=0, ) def __str__(self): return self.title class Meta: ordering = ['order']
class Faq(SearchMetaBase): """ An FAQ """ page = models.ForeignKey(Faqs) question = models.CharField(max_length=256) answer = HtmlField() url_title = models.CharField(max_length=256, unique=True) order = models.PositiveIntegerField(default=0) def __unicode__(self): return self.question class Meta: verbose_name = "faq" verbose_name_plural = "faqs" ordering = ['order', 'id', 'question'] def get_absolute_url(self): """ Gets the url of a Faq Returns: url of Person """ return "{}{}/".format(self.page.page.get_absolute_url(), self.url_title)
class Career(PageBase): page = models.ForeignKey( Careers ) location = models.CharField( max_length=256, blank=True, null=True ) summary = models.TextField( blank=True, null=True ) description = HtmlField() email_address = models.EmailField() order = models.PositiveIntegerField( default=0 ) class Meta: ordering = ['order'] def __str__(self): return self.title def get_absolute_url(self): return self.page.page.reverse('career_detail', kwargs={ 'slug': self.slug, })
class SectionBase(models.Model): page = models.ForeignKey(Page, ) type = models.CharField( choices=get_section_type_choices(SECTION_TYPES), max_length=100, ) title = models.CharField( max_length=140, blank=True, null=True, ) text = models.TextField( blank=True, null=True, ) content = HtmlField( blank=True, null=True, ) image = ImageRefField( blank=True, null=True, ) button_text = models.CharField( max_length=100, blank=True, null=True, ) button_url = models.CharField( 'button URL', max_length=200, blank=True, null=True, ) order = models.PositiveIntegerField( default=0, help_text='Order which the section will be displayed', ) class Meta: abstract = True ordering = ['order'] def __str__(self): return dict(SECTION_TYPES)[self.type]['name'] @property def template(self): return f'{self.type}.html'
class NewsFeed(ContentBase): """A stream of news articles.""" icon = "news/img/news-feed.png" # The heading that the admin places this content under. classifier = "syndication" # The urlconf used to power this content's views. urlconf = "cms.apps.news.urls" content_primary = HtmlField("primary content", blank=True) per_page = models.IntegerField( "articles per page", default=5, blank=True, null=True, )
class Event(PageBase): page = models.ForeignKey(Events, ) start_date = models.DateField() end_date = models.DateField() description = HtmlField() image = ImageRefField( null=True, blank=True, ) class Meta: ordering = ['start_date'] def __str__(self): return self.title def get_absolute_url(self): if self.page: return self.page.page.reverse('event_detail', kwargs={ 'slug': self.slug, }) def get_summary(self): return self.summary @property def date(self): date_string = '{}'.format(date(self.start_date, 'j F Y')) if self.start_date != self.end_date: date_string += ' - {}'.format(date(self.end_date, 'j F Y')) return date_string
class Category(PageBase): """A category for news articles.""" content_primary = HtmlField("primary content", blank=True) def _get_permalink_for_page(self, page): """Returns the URL for this category for the given page.""" return page.reverse("article_category_archive", kwargs={ "url_title": self.url_title, }) def _get_permalinks(self): """Returns a dictionary of all permalinks for the given category.""" pages = Page.objects.filter(id__in=Article.objects.filter( categories=self).values_list("news_feed_id", flat=True)) return dict((u"page_{id}".format(id=page.id), self._get_permalink_for_page(page)) for page in pages) class Meta: verbose_name_plural = "categories" unique_together = (("url_title", ), ) ordering = ("title", )
class Article(PageBase): '''A simple news article.''' objects = ArticleManager() # We want to be able to have multiple types of news feed. For example, # we might want to have a page of articles called "News" (what your cat # did today) vs "Blog" (insights on cat behaviour). Because we have access # to the current page and its content object in our request, we can filter # only the news articles which have this ForeignKey set to the currently # active page - alternatively put, that "belong" to it. page = models.ForeignKey( 'news.NewsFeed', on_delete=models.PROTECT, null=True, blank=False, verbose_name='News feed' ) # ImageRefField is a ForeignKey to `media.File`, but it uses a raw ID # widget by default, and is constrained to only select files that appear # to be images (just a regex on the filename). You also have FileRefField # that doesn't do the "looks like an image" filtering, but does use the # raw ID widget. image = ImageRefField( null=True, blank=True, on_delete=models.PROTECT, ) # HtmlField is like a TextField, but gives you a full-featured TinyMCE # WYSIWYG editor in your admin. This is just for your convenience. # HtmlField is not used internally in the CMS, and nothing about a # onespacemedia-cms project requires that you use it. You can use your own # favourite field with your own favourite editor here. (BUT YOU SHOULD USE # OURS REALLY) content = HtmlField() # Some more standard Django fields :) date = models.DateTimeField( default=now, ) summary = models.TextField( blank=True, ) class Meta: # Not a CMS thing, but if we have two articles assigned to the same # page (as above) they'll both have the same URL, and the # queryset.get(...) will throw MultipleObjectsReturned. Let's stop # that from happening. unique_together = [['page', 'slug']] ordering = ['-date'] def __str__(self): return self.title def get_absolute_url(self): # OK, so once we have our urlconf on our content object, whereever we # we have access to that content, we reverse those URLs almost exactly # as we use django's standard reverse. # # self.page here is our NewsFeed (content model), and self.page.page # is the page to which our content model is attached. return self.page.page.reverse('article_detail', kwargs={ 'slug': self.slug, })
class Article(PageBase): """A news article.""" objects = ArticleManager() news_feed = models.ForeignKey( 'news.NewsFeed', null=True, blank=False, ) featured = models.BooleanField( default=False, ) date = models.DateTimeField( db_index=True, default=timezone.now, ) image = ImageRefField( blank=True, null=True, ) card_image = ImageRefField( blank=True, null=True, help_text="By default the card will try and use the main image, if it doesn't look right you can override it here.", ) content = HtmlField() summary = models.TextField( blank=True, ) call_to_action = models.ForeignKey( 'components.CallToAction', blank=True, null=True, help_text="By default the call to action will be the same as the news feed. You can override it for a specific article here." ) categories = models.ManyToManyField( 'news.Category', blank=True, ) status = models.CharField( max_length=100, choices=STATUS_CHOICES, default='draft', ) class Meta: unique_together = [['news_feed', 'date', 'slug']] ordering = ['-date'] permissions = [ ('can_approve_articles', 'Can approve articles'), ] def __str__(self): return self.short_title or self.title def _get_permalink_for_page(self, page): """Returns the URL of this article for the given news feed page.""" return page.reverse('article_detail', kwargs={ 'slug': self.slug, }) def get_absolute_url(self): """Returns the URL of the article.""" return self._get_permalink_for_page(self.news_feed.page) @property def get_summary(self): summary = self.summary or striptags(truncate_paragraphs(self.content, 1)) return truncatewords(summary, 15) @property def last_modified(self): version = Version.objects.get_for_object(self).first() if version: return version.revision.date_created def render_card(self): return render_to_string('news/includes/card.html', { 'article': self, }) def get_related_articles(self, count=3): related_articles = Article.objects.filter( categories=self.categories.all(), ).exclude( id=self.id ) if related_articles.count() < count: related_articles |= Article.objects.exclude( id__in=[self.pk] + [x.id for x in related_articles] ) return related_articles.distinct()[:count]
class StandardPage(ContentBase): content_primary = HtmlField( blank=True, null=True, )
class Content(ContentBase): content_primary = HtmlField("primary content", blank=True)
class SectionBase(models.Model): page = models.ForeignKey( Page, ) type = models.CharField( choices=get_section_type_choices(SECTION_TYPES), max_length=100, ) background_colour = models.CharField( max_length=255, choices=[ ('white', 'White'), ], default='white', ) kicker = models.CharField( max_length=50, blank=True, null=True, ) title = models.CharField( max_length=140, blank=True, null=True, ) text = models.TextField( blank=True, null=True, ) content = HtmlField( blank=True, null=True, ) image = ImageRefField( blank=True, null=True, ) mobile_image = ImageRefField( blank=True, null=True, ) image_side = models.CharField( max_length=10, choices=[ ('left', 'Left'), ('right', 'Right'), ], default='left', ) link_text = models.CharField( max_length=100, blank=True, null=True, ) link_page = models.ForeignKey( 'pages.Page', blank=True, null=True, help_text='Use this to link to an internal page.', related_name='+' ) link_url = models.CharField( 'link URL', max_length=200, blank=True, null=True, help_text='Use this to link to any other URL.', ) order = models.PositiveIntegerField( default=0, help_text='Order which the section will be displayed', ) class Meta: abstract = True ordering = ['order'] def __str__(self): return next((x for x in get_section_types_flat() if x['slug'] == self.type), None)['name'] def clean(self): sections = get_section_types_flat() for section in sections: if self.type == section['slug']: required = [getattr(self, field) for field in section['required']] if not all(required): fields_str = '' fields_len = len(section['required']) fields = {} for index, field in enumerate(section['required']): fields[field] = ValidationError(f'Please provide an {field}', code='required') connector = ', ' if index == fields_len - 2: connector = ' and ' elif index == fields_len -1: connector = '' anchor = f'id_{self._meta.model_name}_set-{self.order}-{field}' fields_str += f'<a href="#{anchor}">{field.title()}</a>{connector}' fields['__all__'] = ValidationError(mark_safe(f"{fields_str} fields are required"), code='error') raise ValidationError(fields) if self.link_text and (not self.link_page or not self.link_url): raise ValidationError({ 'link_page': 'Please supply 1 of "link page" or "link URL"', }) @property def template(self): folder_name = self.type.split('-')[0] file_name = '-'.join(self.type.split('-')[1:]) return { 'folder': folder_name, 'file_name': f'{file_name}.html' } @property def has_link(self): return self.link_location and self.link_text @cached_property def link_location(self): if self.link_page_id: try: return self.link_page.get_absolute_url() except Page.DoesNotExist: pass return self.link_url def get_searchable_text(self): """Returns a blob of text suitable for searching.""" # Let's look for the options for our section type. for section_group in SECTION_TYPES: for section_type in section_group[1]['sections']: section_label = slugify('{}-{}'.format(section_group[0], section_type[0])) if not section_label == self.type: continue # If we defeated the above clause then we have the options # for the right section type. section_options = section_type[1] # Don't require that search_fields is set. if 'search_fields' not in section_options: continue search_fields = section_options['search_fields'] search_text_items = [] for field in search_fields: search_item = getattr(self, field) if search_item: search_text_items.append(strip_tags(search_item)) return u'\n'.join(search_text_items) return ''
class Setting(models.Model): name = models.CharField( max_length=1024, help_text='Name of the setting', ) key = models.CharField( max_length=1024, help_text='The key used to reference the setting', ) type = models.CharField( max_length=1024, choices=[ ('string', 'String'), ('text', 'Text'), ('html', 'HTML'), ('number', 'Number'), ('image', 'Image'), ], ) string = models.CharField( max_length=2048, blank=True, null=True, ) text = models.TextField( blank=True, null=True, ) html = HtmlField( 'HTML', null=True, blank=True, ) number = models.IntegerField( blank=True, null=True, ) image = ImageRefField( blank=True, null=True, ) class Meta: ordering = ['name'] def __str__(self): return self.name @cached_property def value(self): return { 'string': self.string, 'text': linebreaksbr(mark_safe(self.text)), 'html': mark_safe(self.html), 'number': self.number, 'image': self.image if self.image else '', }[self.type]