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 TestModelBase(models.Model): image = ImageRefField( blank=True, null=True, on_delete=models.CASCADE, ) class Meta: abstract = True
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 ContentColumn(models.Model): page = models.ForeignKey(Page) title = models.CharField(max_length=100, ) url = models.CharField( "URL", max_length=100, ) image = ImageRefField()
class Partner(PageBase): summary = models.TextField(max_length=140, blank=True, null=True) logo = ImageRefField() website = models.CharField(max_length=140, blank=True, null=True) order = models.PositiveIntegerField(default=0) class Meta: ordering = ['order'] def __str__(self): return self.title
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 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 SearchMetaBase(OnlineBase): """Base model for models used to generate a standalone HTML page.""" objects = SearchMetaBaseManager() # SEO fields. browser_title = models.CharField( max_length=1000, blank=True, help_text=( "The heading to use in the user's web browser. " "Leave blank to use the page title. " "Search engines pay particular attention to this attribute.")) meta_description = models.TextField( "description", blank=True, help_text="A brief description of the contents of this page.", ) sitemap_priority = models.FloatField( "priority", choices=( (1.0, "Very high"), (0.8, "High"), (0.5, "Medium"), (0.3, "Low"), (0.0, "Very low"), ), default=None, blank=True, null=True, help_text= ("The relative importance of this content on your site. Search engines use this " "as a hint when ranking the pages within your site."), ) sitemap_changefreq = models.IntegerField( "change frequency", choices=((1, "Always"), (2, "Hourly"), (3, "Daily"), (4, "Weekly"), (5, "Monthly"), (6, "Yearly"), (7, "Never")), default=None, blank=True, null=True, help_text= ("How frequently you expect this content to be updated. " "Search engines use this as a hint when scanning your site for updates." ), ) robots_index = models.BooleanField( "allow indexing", default=True, help_text= ("Uncheck to prevent search engines from indexing this page. " "Do this only if the page contains information which you do not wish " "to show up in search results."), ) robots_follow = models.BooleanField( "follow links", default=True, help_text= ("Uncheck to prevent search engines from following any links they find in this page. " "Do this only if the page contains links to other sites that you do not wish to " "publicise."), ) robots_archive = models.BooleanField( "allow archiving", default=True, help_text= ("Uncheck this to prevent search engines from archiving this page. " "Do this this only if the page is likely to change on a very regular basis. " ), ) # Open Graph fields og_title = models.CharField( verbose_name='title', blank=True, max_length=100, help_text= 'Title that will appear on Facebook posts. This is limited to 100 characters, ' 'but Facebook will truncate the title to 88 characters.') og_description = models.TextField( verbose_name='description', blank=True, max_length=300, help_text= 'Description that will appear on Facebook posts. It is limited to 300 ' 'characters, but it is recommended that you do not use anything over 200.' ) og_image = ImageRefField( verbose_name='image', blank=True, null=True, help_text= 'The recommended image size is 1200x627 (1.91:1 ratio); this gives you a big ' 'stand out thumbnail. Using an image smaller than 400x209 will give you a ' 'small thumbnail and will splits posts into 2 columns. ' 'If you have text on the image make sure it is centered.') # Twitter card fields twitter_card = models.IntegerField( verbose_name='card', choices=[ (0, 'Summary'), (1, 'Photo'), (2, 'Video'), (3, 'Product'), (4, 'App'), (5, 'Gallery'), (6, 'Large Summary'), ], blank=True, null=True, default=None, help_text= 'The type of content on the page. Most of the time "Summary" will suffice. ' 'Before you can benefit from any of these fields make sure to go to ' 'https://dev.twitter.com/docs/cards/validation/validator and get approved.' ) twitter_title = models.CharField( verbose_name='title', blank=True, max_length=70, help_text= 'The title that appears on the Twitter card, it is limited to 70 characters.' ) twitter_description = models.TextField( verbose_name='description', blank=True, max_length=200, help_text='Description that will appear on Twitter cards. It is limited ' 'to 200 characters. This does\'nt effect SEO, so focus on copy ' 'that complements the tweet and title rather than on keywords.') twitter_image = ImageRefField( verbose_name='image', blank=True, null=True, help_text= 'The minimum size it needs to be is 280x150. If you want to use a larger image' 'make sure the card type is set to "Large Summary".') def get_context_data(self): """Returns the SEO context data for this page.""" title = str(self) # Return the context. return { "meta_description": self.meta_description, "robots_index": self.robots_index, "robots_archive": self.robots_archive, "robots_follow": self.robots_follow, "title": self.browser_title or title, "header": title, "og_title": self.og_title, "og_description": self.og_description, "og_image": self.og_image, "twitter_card": self.twitter_card, "twitter_title": self.twitter_title, "twitter_description": self.twitter_description, "twitter_image": self.twitter_image } def render(self, request, template, context=None, **kwargs): """Renders a template as a HttpResponse using the context of this page.""" page_context = self.get_context_data() page_context.update(context or {}) return render(request, template, page_context, **kwargs) class Meta: abstract = 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]