from django.db import models from django.conf import settings from django import forms from stat import * from urllib import urlopen, urlretrieve, urlcleanup, ContentTooShortError from PIL import Image from django.utils.functional import lazy import datetime, os, shutil, urllib def comparePubs(pub1, pub2): comp = cmp(pub2.venue_instance.get_date(), pub1.venue_instance.get_date()) if comp == 0: comp = cmp(pub1.title, pub2.title) return comp class Affiliation(models.Model): abbreviation = models.CharField(max_length=1000, blank=False, unique=True, help_text="Abbreviation or short name of this affiliation, e.g. \"CSE\".") full_name = models.CharField(max_length=1000, blank=True, unique=True, help_text="Full name of this affiliation, e.g. \"Computer Science and Engineering\".") url = models.URLField(blank=True, help_text="URL of the homepage for this affiliation, e.g. \"http://www.cs.washington.edu\".") def __unicode__(self): return self.abbreviation + ' (' + self.full_name + ')' class Meta: ordering = ['abbreviation'] class ResearchArea(models.Model): name = models.CharField("research area", max_length=100, help_text="Name of the research area. This will be used to categorize the projects in the Project page.") def __unicode__(self): return self.name class Meta: ordering = ['name'] class BibTexPublicationType(models.Model): name = models.CharField("publication type", help_text="BibTex publication type name, e.g. article, book, inproceedings, etc.", max_length=100, unique=True) def __unicode__(self): return self.name class Meta: ordering = ['name'] verbose_name = "BibTex supported publication type" class PersonRole(models.Model): name = models.CharField("role name", help_text="Name of the role that a person can belong to.", max_length=100, blank=False) def __unicode__(self): return self.name def get_persons_in_role(self): return Person.objects.filter(role=self) def get_person_role(cls, roleToFind): try: result = cls.objects.get(name=roleToFind) except: result = None return result get_person_role = classmethod(get_person_role) class Meta: ordering = ['name'] verbose_name = "individual role" class Person(models.Model): is_ai_member = models.BooleanField("appears on the official http://ai.cs.washington.edu website under the People section", help_text="Leave unchecked for a co-author who is not in AI.") first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) middle_initial = models.CharField("middle initial", blank=True, max_length=50, help_text="Enter middle initials here if desired. Include trailing periods, e.g. \"A.F.\"") email_address = models.EmailField(blank=True, help_text="Comments about pubs you author get sent here. Also appears on your personal AI page.") homepage_url = models.URLField("external homepage URL", help_text="E.g. http://www.cs.washington.edu/homes/johnsmith", blank=True) affiliation = models.ForeignKey(Affiliation, verbose_name="primary afiliation", blank=True, null=True, help_text="If this person is affiliated with multiple groups, choose the primary affiliation.
Leave blank if appropriate affiliation is not listed.") secondary_affiliations = models.ManyToManyField(Affiliation, blank=True, related_name='secondary_affiliation_set', help_text="To unselect a selection, hold down \"Control\", or \"Command\" on a Mac, and click on the selection to unselect.
") role = models.ForeignKey(PersonRole, verbose_name="UW title", blank=True, null=True, help_text="Leave blank if not part of UW or if your role is not listed.") ai_url = models.SlugField(verbose_name="AI URL", unique=True, blank=True, help_text="Your personal AI page will automatically be generated at http://ai.cs.washington.edu/projects/<AI URL name>.
You can change this at any time.") photo = models.ImageField(upload_to="icons/people", height_field=0, width_field=0, blank=True, help_text="Currently will be resized to 120 pixels wide (in browser).") bio = models.TextField(blank=True, help_text="Enter the main content that you would like to show in your personal AI page.
The text you enter here will be interpreted as raw HTML so be sure to use appropriate escape sequences, such as &amp; for the ampersand sign.

Sample entry:

I am a fourth-year PhD student in the <a href="http://www.cs.washington.edu">Computer Science &amp; Engineering</a>
department at the <a href="http://www.washington.edu">University of Washington</a> advised by
<a href="/people/landay">Prof. James Landay</a>. My research interests are in
Human-Computer Interaction (HCI), Ubiquitous Computing (UbiComp), physiological I/O channels, prototyping tools for
designers, and computer security and privacy.


will show up as the following on your project page:

I am a fourth-year PhD student in the Computer Science & Engineering department at the University of Washington advised by Prof. James Landay. My research interests are in Human-Computer Interaction (HCI), Ubiquitous Computing (UbiComp), physiological I/O channels, prototyping tools for designers, and computer security and privacy.") keywords = models.CharField("interest keywords", max_length=1000, blank=True, help_text="List of keywords for your area of interest, separate by commas") def __unicode__(self): return self.get_fullname() def get_fullname(self): if self.middle_initial: initial = self.middle_initial + ' ' else: initial = '' return self.first_name + ' ' + initial + self.last_name get_fullname.admin_order_field = 'last_name' def get_safe_email_address(self): return self.email_address.replace("@", " [at] ") def get_publications(self): pubs = [author.publication for author in Author.objects.filter(person=self).exclude(publication__venue_instance__venue__full_name__startswith="AI")] pubs.sort(cmp=comparePubs) return pubs def get_projects(self): return Project.objects.filter(collaborators=self) def get_ai_url(self): return '/people/' + self.ai_url def get_person_by_ai_url(cls, url): try: result = cls.objects.get(ai_url=url) except: result = None return result get_person_by_ai_url = classmethod(get_person_by_ai_url) class Meta: ordering = ['last_name', 'first_name'] unique_together = [("first_name", "middle_initial", "last_name")] verbose_name_plural = "people" class MediaType(models.Model): name = models.CharField("media type", max_length=100, help_text="Short name for the type of media, such as 'Video', 'Slides', etc.", unique=True) extensions = models.CharField("filetype extensions", max_length=200, help_text="Comma-delimited list of filetpype extensions that fall under this media type.
Be sure to include the leading period before the extension names.
E.g. for 'Video' media type, \".avi,.mpg,.wmv,.mov\""); icon = models.ImageField(upload_to='icons/mediatypes', height_field=0, width_field=0, blank=True, help_text="Optional icon to display representing this media type") def get_extensions(self): extensions = self.extensions.split(',') if len(extensions) == 1 and len(extensions[0]) == 0: extensions = [] return extensions def __unicode__(self): return self.name class Meta: ordering = ['name'] class DownloadableMedia(models.Model): # MAX_DESCRIPTION_LENGTH = 40 media_file = models.FileField("media file", upload_to='downloadable/media', help_text="The \"type\" of the media you upload (e.g. \"Video\", \"Slides\") will be determined automatically based on the file's extension (e.g. \".avi\", \".ppt\").
To see the list of currently defined Media Types, or to create a new one, go back to the main Admin page and click on \"Media types\".
Any media file you upload that does not have an associated media type will be listed as \"Unknown\" media type.") description = models.CharField("short description", max_length=200, unique=True, help_text="This text is used to help identify this particular media among others when listing all available media to choose from.
Choose concise, unique description text.
This text is different from the caption that will be shown to the website visitors when they come across the media.") caption = models.CharField("caption", max_length=300, blank=True, help_text="This is the text that will be shown to the site visitors next to the link to this media.
E.g. \"Video showing the use of pen gesture for specifying simultaneous translation and rotation (320x240, AVI)\"") def get_media_file_filename(self): return self.media_file.name def get_filename(self): return os.path.basename(self.get_media_file_filename()) def get_file_extension(self): return os.path.splitext(self.get_filename())[1]; def get_filesize_in_mb(self): return self.get_media_file_size() / 1048576.0; def get_media_type(self): extension = self.get_file_extension().lower() for mediaType in MediaType.objects.all(): if extension in mediaType.get_extensions(): return mediaType return None def __unicode__(self): #name = os.path.basename(self.get_media_file_filename()) + " (" + self.media_type.name + ")" mediaType = self.get_media_type() name = "(Unknown): " + self.get_filename() + " - " if mediaType: name = "(" + mediaType.name + "): " if (self.description): name = name + self.description # name = name + self.description[:self.MAX_DESCRIPTION_LENGTH] # if (len(self.description) > self.MAX_DESCRIPTION_LENGTH): # name = name + "..." else: name = name + self.get_filename() return name class Project(models.Model): name = models.CharField("project name", max_length=200, help_text="This will be the name which will be displayed under the list of projects and as a heading at the top of your project page.") ai_url = models.SlugField("AI URL name", unique=True, help_text="Your project page will automatically be generated at http://ai.cs.washington.edu/projects/<AI URL name>.
This name is auto-generated based on the project name you entered above, but you can change it to whatever you wish, as long as another project does not already exist with the same name.
You can modify this at a later time if you wish.") description = models.TextField("main text", help_text="Enter the main content that you would like to show in your project page.
The text you enter here will be interpreted as raw HTML so be sure to use appropriate escape sequences, such as &amp; for the ampersand sign.

Sample entry:

<i>DENIM</i> is a system that helps web site designers in the early stages of <a href=\"http://en.wikipedia.org/wiki/Design\">design</a>.
<i>DENIM</i> supports sketching input, allows design at different refinement levels, &amp; unifies the levels through zooming.
<br><br>
<b>Current DENIM System</b><br>
<ul>
<li>RealPlayer G2, 8.0, or RealOne Player:
    <a href=\"http://www.denim.org/denim_56.ram\">56 Kbps - 320x240</a> |
    <a href=\"http://www.denim.org/denim_300.ram\">300 Kbps - 640x480</a>
</li>
<li>Excerpt - <a href=\"http://www.denim.org/denim_talk.rm\">Downloadable RealVideo file (14.3 MB)</a></li>
</ul>


will show up as the following on your project page:

DENIM is a system that helps web site designers in the early stages of design.
DENIM supports sketching input, allows design at different refinement levels, & unifies the levels through zooming.

Current DENIM System
") icon = models.ImageField(upload_to='icons/projects', height_field=0, width_field=0, blank=True, help_text="Currently will be resized to 120 pixels wide (in browser), but this might change with a AI website redesign.") external_url = models.URLField("external project URL", blank=True, help_text="E.g. http://www.denim.org/
If you have a separate project homepage outside of ai.cs.washington.edu that you would like a link to, enter it here.") screenshot = models.ImageField(upload_to='screenshots/projects', height_field=0, width_field=0, blank=True, help_text="This image will be shown as the primary screenshot for the project.
NOTE: If the image is larger than 400 pixels tall or wide, it will automatically be scaled to fit.") screenshot_caption = models.CharField(max_length=300, blank=True, help_text="Caption text that will be displayed below the main screenshot.") collaborators = models.ManyToManyField(Person, blank=True) research_area = models.ManyToManyField(ResearchArea,\ blank=True, \ help_text="Try to choose just one."\ +"Your project will appear under these categories on the ai.cs.washington.edu/projects page."\ +"The webmaster consolidates research areas periodically; give him feedback at web@ai.cs.washington.edu."\ +"
To unselect a selection, hold down \"Control\", or \"Command\" on a Mac, and click on the selection to unselect.
") medias = models.ManyToManyField(DownloadableMedia, blank=True, help_text="Select the media you wish to add to this project.") external_download_url = models.URLField("external download URL", blank=True, help_text="E.g. http://www.denim.org/downloads/
If you have a separate webpage that contains links to downloads for your project, enter it here.") def __unicode__(self): return self.name def get_ai_url(self): return '/projects/' + self.ai_url def get_project_by_ai_url(cls, url): try: result = cls.objects.get(ai_url=url) except: result = None return result get_project_by_ai_url = classmethod(get_project_by_ai_url) def save(self): if self.screenshot: original_path_dir = os.path.dirname(self.screenshot) screenshot_path = os.path.join(settings.MEDIA_ROOT, self.screenshot) screenshot_dir = os.path.dirname(screenshot_path) screenshot_filename = os.path.basename(screenshot_path) screenshot_filename_base = screenshot_filename.split('.')[-2] screenshot_filename_ext = screenshot_filename.split('.')[-1] screenshot_img = Image.open(screenshot_path) width, height = screenshot_img.size if width > 400 or height > 400: screenshot_img.thumbnail((400, 400), Image.ANTIALIAS) newwidth, newheight = screenshot_img.size new_filename = screenshot_filename_base + str(newwidth) + 'x' + str(newheight) + '.' + screenshot_filename_ext screenshot_img.save(os.path.join(screenshot_dir, new_filename), screenshot_img.format) self.screenshot = os.path.join(original_path_dir, new_filename) super(Project, self).save() class Meta: ordering = ['name'] class CourseType(models.Model): type = models.CharField("course type", max_length=200, unique=True, help_text="Type of course") def __unicode__(self): return self.type class Meta: ordering = ['type'] class Course(models.Model): type = models.ForeignKey(CourseType, help_text="Type of course") affiliation = models.ForeignKey(Affiliation, help_text="The department responsible for the course, i.e. CSE, EE") course_number = models.CharField("course number", max_length=5, unique=True, help_text="The number of this course") class CourseInstance(models.Model): course = models.ForeignKey(Course, help_text="Course department and number") QUARTER_CHOICES = ( ('au', 'Autumn'), ('wi', 'Winter'), ('sp', 'Spring'), ('su', 'Summer'), ) quarter = models.CharField(max_length=2, choices=QUARTER_CHOICES, help_text="The quarter corresponding to this course instance") year = models.IntegerField(choices=[(i, i) for i in xrange(2000, datetime.date.today().year + 5)], help_text="The year corresponding to this course instance") def get_course_url(self): return 'http://www.cs.washington.edu/education/courses/' + course.affiliation.abbr + '/' + self.year + self.quarter + '/' class Venue(models.Model): full_name = models.CharField("full name", max_length=200, unique=True, help_text="The full name of the venue. E.g.:
ACM SIGCHI Conference on Human Factors in Computing Systems") abbreviation = models.CharField("abbreviation", max_length=40, unique=True, help_text="The abbreviation or short name for the venue, e.g. CHI, UIST, etc.") def __unicode__(self): return self.abbreviation def get_publication_types(self): return VenuePublicationType.objects.filter(venue=self).order_by('order') class Meta: ordering = ['abbreviation'] class VenuePublicationType(models.Model): venue = models.ForeignKey(Venue, help_text="The venue that this publication type belongs to") name = models.CharField("publication type", max_length=100, help_text="The specific type of publication available for the venue chosen above, e.g. \"Full Paper\", \"Poster\", \"Journal Article\".") order = models.IntegerField(choices=[(i, i) for i in xrange(1,20)], default=1, help_text="The position in the ordering of all publication types associated with the specified venue within which this publication type will be listed.
If multiple publication types have the same order number, they will be ordered alphabetically.
Leave as '1' if uncertain.") def __unicode__(self): return self.venue.abbreviation + " " + self.name class Meta: ordering = ['venue', 'order', 'name'] unique_together = [("name", "venue")] verbose_name = "venue-specific publication type" class VenueInstance(models.Model): venue = models.ForeignKey(Venue, verbose_name="venue name", help_text="The conference or journal that this instance belongs to.") year = models.IntegerField(choices=[(i, i) for i in xrange(2000, datetime.date.today().year + 5)], help_text="The year corresponding to this venue instance.") month = models.IntegerField(blank=True, null=True, choices=[(i, i) for i in xrange(1,13)], help_text="The month corresponding to this venue instance. Leave blank if unknown (e.g. for a journal). You can always come back and update this once the date is known.") url = models.URLField("homepage URL", blank=True, help_text="Homepage for this venue instance (e.g. http://chi2008.org)") city = models.CharField(max_length=100, blank=True) state = models.CharField(max_length=100, blank=True) country = models.CharField(max_length=100, blank=True) icon = models.ImageField(upload_to='icons/venues', height_field=0, width_field=0, blank=True, help_text='Icon representing this venue instance') def __unicode__(self): return self.get_short_name() def get_long_name(self): return self.venue.full_name + " " + str(self.year) def get_short_name(self): return self.venue.abbreviation + " " + str(self.year) def get_date(self): mo = self.month da = 1 if self.month == None: mo = 12 da = 31 return datetime.date(self.year, mo, da) def is_location_specified(self): return self.city or self.state or self.country def get_location(self): location = "" if self.city: location = self.city if self.state: if location == "": location = self.state else: location = location + ", " + self.state if self.country: if location == "": location = self.country else: location = location + ", " + self.country return location def get_publication_types(self): return VenuePublicationType.objects.filter(venue=self.venue) def get_publications(self): return external_publications().filter(venue_publication_type__in=self.get_publication_types(), venue_instance=self) class Meta: ordering = ['-year', '-month'] class PublicationAward(models.Model): venue = models.ForeignKey(Venue, help_text="The conference or journal in which this award is given.") name = models.CharField("award name", max_length=100, help_text="Name of the award, e.g. \"Best Paper Award\".
The venue name above will be prepended to the award name when displayed, so you do not need to include it here.") description = models.CharField(max_length=400, blank=True, help_text="Descriptive text for the award, e.g. \"Awarded to papers deemed to make an especially noteworthy contribution to human-computer interaction research\"") icon = models.ImageField(upload_to='icons/awards', height_field=0, width_field=0, blank=True, help_text="Optional icon to display representing this award. This will appear next to the publication which received this award in the publications list.") def __unicode__(self): return self.venue.abbreviation + " " + self.name class Meta: # Funky syntax for ordering PublicationAward defined here: # http://groups.google.com/group/django-users/browse_frm/thread/e79a1424f6519b9b# # Syntax likely to change in a future version #ordering = ['main__publicationaward.venue'] pass class Publication(models.Model): def get_slug(self): return '/pubs/' + str(self.id) def validate_pdf_url(field_data, all_data): # Make sure the URL points to a downloadable PDF if field_data: try: # get the content type instream = urlopen(field_data) except: raise forms.ValidationError("Error retrieving the PDF at the specified URL. Verify that the file is accessible.") else: try: type = instream.info().getheader("Content-Type") if type != 'application/pdf': raise forms.ValidationError("PDF URL must point to a valid PDF file.") except KeyError: raise forms.ValidationError("PDF URL must point to a valid PDF file.") venue_instance = models.ForeignKey(VenueInstance, verbose_name="venue", help_text="The specific venue at which this publication was published") venue_publication_type = models.ForeignKey(VenuePublicationType, verbose_name="publication type", help_text="The type of publication (specific to a particular venue)") title = models.CharField("title", max_length=200) pdf = models.FileField(verbose_name="PDF", upload_to='papers', blank=True, help_text="PDF file that you would like this publication to link to.
If you do not have a local copy to upload but have an URL at which it is available, you can enter the URL below.") pdf_url = models.URLField("URL to PDF", verify_exists=True, blank=True, help_text="You can specify the URL to the PDF here if you do not have a local copy. Make sure it is downloadable without any authentication.") icon = models.ImageField(upload_to='icons/pubs', blank=True, help_text="Currently will be resized to 120 pixels wide (in browser), but this might change with a AI website redesign. (Supported Formats: PNG, JPG, GIF)") doi_url = models.URLField("official publisher's URL", verify_exists=True, blank=True, help_text="E.g. on ACM webpage, http://doi.acm.org/10.1145/1322192.1322225") awards = models.ManyToManyField(PublicationAward, blank=True) projects = models.ManyToManyField(Project, blank=True) bibtex_publication_type = models.ForeignKey(BibTexPublicationType, blank=True, null=True, help_text="Publication type in BibTex") bibtex_address = models.CharField("publisher's address", max_length=100, blank=True, help_text="Publisher's address (usually just the city, but can be the full address for lesser-known publishers.)") bibtex_booktitle = models.CharField("book title", max_length=100, blank=True, help_text="The title of the book, if only part of it is being cited.") bibtex_chapter = models.IntegerField("chapter", blank=True, null=True, help_text="The chapter number.") bibtex_edition = models.CharField("edition", max_length=50, blank=True, help_text="The edition of a book, long form (such as 'first' or 'second').") bibtex_howpublished = models.CharField("how published", max_length=100, blank=True, help_text="How it was published, if the publishing method is nonstandard.") bibtex_institution = models.CharField("institution", max_length=200, blank=True, help_text="The institution that was involved in the publishing, but not necessarily the publisher") bibtex_journal = models.CharField("journal", max_length=200, blank=True, help_text="The journal or magazine the work was published in") bibtex_volume = models.IntegerField("volume", blank=True, null=True, help_text="The volume of a journal or multi-volume book") bibtex_number = models.IntegerField("number", blank=True, null=True, help_text="The \"number\" of a journal, magazine, or tech-report, if applicable. (Most publications have a \"volume\", but no \"number\" field.)") bibtex_organization = models.CharField("organization", max_length=100, blank=True, help_text="The conference sponsor") bibtex_startpage = models.IntegerField("start page", blank=True, null=True, help_text="First page of the page numbers") bibtex_endpage = models.IntegerField("end page", blank=True, null=True, help_text="Last page of the page numbers") bibtex_publisher = models.CharField("publisher", max_length=100, blank=True, help_text="The publisher's name") bibtex_school = models.CharField("school", max_length=200, blank=True, help_text="The school where the thesis was written") bibtex_series = models.CharField("series", max_length=100, blank=True, help_text="The series of books the book was published in (e.g. \"The Hardy Boys\")") bibtex_type = models.CharField("type", max_length=100, blank=True, help_text="The type of tech-report, for example, \"Research Note\"") bibtex_url = models.CharField("URL", max_length=300, blank=True, help_text="The WWW address") bibtex_note = models.TextField("note", blank=True, help_text="Miscellaneous extra information") bibtex_source = models.TextField("BibTex source", blank=True, help_text="Actual BibTex entry text corresponding to this publication") medias = models.ManyToManyField(DownloadableMedia, blank=True, help_text="If you added a media elsewhere but they do not show up here, try clicking the reload button on the browser. In that case, you may need to reselect the publication type from the drop down menu above.") def __unicode__(self): return self.title def get_authors(self): return Author.objects.filter(publication=self) get_authors.short_description = 'Authors' def get_venue(self): return self.venue_instance.venue def get_venue_instance(self): return self.venue_instance def get_venue_publication_type(self): return self.venue_publication_type def get_pdf_url(self): if self.pdf: return self.pdf.url if self.pdf_url: return self.pdf_url return None def save(self): if self.pdf_url: # download the PDF try: (filepath, headers) = urlretrieve(self.pdf_url) # prepare a unique filename to store in the media directory pdfname = filepath.split('/')[-1] papersdir = os.path.join(settings.MEDIA_ROOT, 'papers') while os.path.exists(os.path.join(papersdir, pdfname)): pdfnameparts = pdfname.split('.') pdfname = '.'.join(pdfnameparts[:-1]) + '_.' + pdfnameparts[-1] pdfpath = os.path.join(papersdir, pdfname) try: shutil.move(filepath, pdfpath) os.chmod(pdfpath, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH) except IOError: pass else: self.pdf = os.path.join('papers', pdfname) self.pdf_url = "" super(Publication, self).save() except ContentTooShortError: pass else: urlcleanup() else: super(Publication, self).save() # Call the "real" save() method. class Meta: ordering = ['title'] def public_publications(): return Publication.objects.exclude(venue_instance__venue__full_name__startswith="Dub Retreat") def internal_publications(): return Publication.objects.filter(venue_instance__venue__full_name__startswith="Dub Retreat") def public_venues(): return VenueInstance.objects.all().exclude(venue__full_name__startswith="Dub Retreat") class Author(models.Model): MAX_NUM_AUTHORS = 20 person = models.ForeignKey(Person) # had core=true publication = models.ForeignKey(Publication) # FIX edit_inline=models.TABULAR, num_in_admin=5, max_num_in_admin=MAX_NUM_AUTHORS, num_extra_on_change=5 # author_number had core=true too author_number = models.IntegerField(choices=[(i, i) for i in xrange(1,MAX_NUM_AUTHORS)], help_text="Authors should be ordered starting at 1") def __unicode__(self): return self.person.__unicode__() + ' #' + str(self.author_number) + ': \"' + self.publication.title + '\"' fields = ('person', 'author_number') class Meta: ordering = ['publication', 'author_number'] verbose_name = "ordered authors in publication. The authors will be displayed for this publication in the order added here. If you added a person elsewhere but they do not show up here, try clicking on \"Save and continue editing\" below. If you need to add more authors, fill in all visible author drop downs and then click on \"Save and continue editing\" to reveal extra author slots." verbose_name_plural = verbose_name class NewsItem(models.Model): date = models.DateField() title = models.TextField() image = models.ImageField(upload_to='images/news', blank=True, help_text='Optional. You can also include fancy image layouts by using html in article body.') teaser = models.TextField(help_text='Users see this until they click "Read more". Usually just copy & paste the first paragraph of article here.', verbose_name='Article teaser') body = models.TextField(verbose_name='Full article', help_text='When user clicks "read more" they get this. Go ahead and put lots of interesting content in here.') front_page = models.BooleanField(verbose_name='Appears on AI homepage') def __unicode__(self): return self.title class Meta: ordering = ['-date']