From 94da5e3b53b40ddb6092a72d0fdcda2113c7733c Mon Sep 17 00:00:00 2001 From: Ross Bruniges Date: Fri, 8 Feb 2013 17:48:14 +0000 Subject: [PATCH] Bug 68 - adding in support for project groups --- make_mozilla/projects/admin.py | 12 ++ ...04_auto__add_groupmembership__add_group.py | 143 ++++++++++++++++++ make_mozilla/projects/models.py | 56 ++++++- .../projects/templates/projects/detail.html | 8 + .../templates/projects/group_index.html | 25 +++ .../projects/groups/test_project.html | 25 +++ .../projects/templates/projects/index.html | 7 + make_mozilla/projects/urls.py | 2 + make_mozilla/projects/views.py | 17 ++- media/css/base.less | 61 +++++++- 10 files changed, 349 insertions(+), 7 deletions(-) create mode 100644 make_mozilla/projects/migrations/0004_auto__add_groupmembership__add_group.py create mode 100644 make_mozilla/projects/templates/projects/group_index.html create mode 100644 make_mozilla/projects/templates/projects/groups/test_project.html diff --git a/make_mozilla/projects/admin.py b/make_mozilla/projects/admin.py index 9935eec..886ceee 100644 --- a/make_mozilla/projects/admin.py +++ b/make_mozilla/projects/admin.py @@ -28,6 +28,16 @@ class TagAdmin(admin.ModelAdmin): ) +class GroupAdmin(admin.ModelAdmin): + prepopulated_fields = {'slug': ('name', ), } + + +class GroupMembershipAdmin(admin.ModelAdmin): + list_display = ('project', 'group', 'order', ) + list_editable = ('order', ) + list_filter = ('group', ) + + class TopicAdmin(TagAdmin): pass @@ -45,6 +55,8 @@ class ContributorAdmin(admin.ModelAdmin): admin.site.register(models.Project, ProjectAdmin) +admin.site.register(models.Group, GroupAdmin) +admin.site.register(models.GroupMembership, GroupMembershipAdmin) admin.site.register(models.Topic, TopicAdmin) admin.site.register(models.Difficulty, DifficultyAdmin) admin.site.register(models.Skill, SkillAdmin) diff --git a/make_mozilla/projects/migrations/0004_auto__add_groupmembership__add_group.py b/make_mozilla/projects/migrations/0004_auto__add_groupmembership__add_group.py new file mode 100644 index 0000000..0e3c338 --- /dev/null +++ b/make_mozilla/projects/migrations/0004_auto__add_groupmembership__add_group.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'GroupMembership' + db.create_table('projects_groupmembership', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('project', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['projects.Project'])), + ('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['projects.Group'])), + ('order', self.gf('django.db.models.fields.IntegerField')(default=1, max_length=2)), + )) + db.send_create_signal('projects', ['GroupMembership']) + + # Adding model 'Group' + db.create_table('projects_group', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('slug', self.gf('django.db.models.fields.SlugField')(default='', unique=True, max_length=50)), + ('body', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('body_text_template', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)), + ('take_body_from', self.gf('django.db.models.fields.CharField')(default='body', max_length=4)), + ('image', self.gf('make_mozilla.core.fields.SizedImageField')(max_length=100)), + )) + db.send_create_signal('projects', ['Group']) + + def backwards(self, orm): + # Deleting model 'GroupMembership' + db.delete_table('projects_groupmembership') + + # Deleting model 'Group' + db.delete_table('projects_group') + + models = { + 'events.campaign': { + 'Meta': {'object_name': 'Campaign'}, + 'description': ('django.db.models.fields.TextField', [], {}), + 'end': ('django.db.models.fields.DateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'logo': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'start': ('django.db.models.fields.DateField', [], {}) + }, + 'events.partner': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Partner'}, + 'featured': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'for_campaign': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['events.Campaign']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'logo': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + }, + 'projects.contributor': { + 'Meta': {'object_name': 'Contributor'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'local_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'partner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['events.Partner']", 'null': 'True', 'blank': 'True'}) + }, + 'projects.difficulty': { + 'Meta': {'ordering': "['index', 'label', 'id']", 'object_name': 'Difficulty'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'default': '1', 'max_length': '2'}), + 'label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'value': ('django.db.models.fields.SlugField', [], {'max_length': '50'}) + }, + 'projects.group': { + 'Meta': {'object_name': 'Group'}, + 'body': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'body_text_template': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('make_mozilla.core.fields.SizedImageField', [], {'max_length': '100'}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['projects.Project']", 'through': "orm['projects.GroupMembership']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'default': "''", 'unique': 'True', 'max_length': '50'}), + 'take_body_from': ('django.db.models.fields.CharField', [], {'default': "'body'", 'max_length': '4'}) + }, + 'projects.groupmembership': { + 'Meta': {'object_name': 'GroupMembership'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1', 'max_length': '2'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']"}) + }, + 'projects.project': { + 'Meta': {'ordering': "['-added']", 'object_name': 'Project'}, + 'added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'body': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'contributor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Contributor']", 'null': 'True', 'blank': 'True'}), + 'difficulties': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['projects.Difficulty']", 'null': 'True', 'blank': 'True'}), + 'featured': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('make_mozilla.core.fields.SizedImageField', [], {'max_length': '100'}), + 'link': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'skills': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['projects.Skill']", 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'default': "''", 'unique': 'True', 'max_length': '50'}), + 'teaser': ('django.db.models.fields.TextField', [], {}), + 'tools': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['tools.Tool']", 'null': 'True', 'blank': 'True'}), + 'topics': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['projects.Topic']", 'null': 'True', 'blank': 'True'}), + 'url_hash': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'}) + }, + 'projects.projectstep': { + 'Meta': {'object_name': 'ProjectStep'}, + 'content': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'steps'", 'to': "orm['projects.Project']"}) + }, + 'projects.skill': { + 'Meta': {'ordering': "['index', 'label', 'id']", 'object_name': 'Skill'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'default': '1', 'max_length': '2'}), + 'label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'value': ('django.db.models.fields.SlugField', [], {'max_length': '50'}) + }, + 'projects.topic': { + 'Meta': {'ordering': "['index', 'label', 'id']", 'object_name': 'Topic'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.IntegerField', [], {'default': '1', 'max_length': '2'}), + 'label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'value': ('django.db.models.fields.SlugField', [], {'max_length': '50'}) + }, + 'tools.tool': { + 'Meta': {'ordering': "['name']", 'object_name': 'Tool'}, + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'featured': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'logo': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'new': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'strapline': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['projects'] \ No newline at end of file diff --git a/make_mozilla/projects/models.py b/make_mozilla/projects/models.py index bee2797..af1fd02 100644 --- a/make_mozilla/projects/models.py +++ b/make_mozilla/projects/models.py @@ -60,6 +60,14 @@ def get_absolute_url(self): return self.link return reverse('project', kwargs={'slug': self.slug}) + @property + def groups(self): + try: + return self.group_set.all() + # because we have the 'dummy project' that doesn't have a primary key + except ValueError: + return None + @property def content(self): if self.steps.count(): @@ -91,6 +99,50 @@ def next(self): return None +class Group(models.Model): + name = models.CharField(max_length=255) + slug = models.SlugField(unique=True, default='', + help_text="Remember if you change this after a group has been saved it can cause broken links") + members = models.ManyToManyField(Project, through='GroupMembership') + body = models.TextField(null=True, blank=True, + help_text='Please paste your HTML in here - please remember to include any images behind an HTTPS server') + body_text_template = models.CharField(max_length=255, blank=True, null=True, + help_text="If supplying an HTML template for the body include the filename here. We look for templates in 'make_mozilla/projects/templates/groups/'") + take_body_from = models.CharField(max_length=4, choices=( + ('body', 'body'), + ('temp', 'body_text_template') + ), + default='body', + help_text='Do you want the introduction text to be taken direct from the DB or from an HTML template file?') + image = fields.SizedImageField( + upload_to='projects', + help_text='Unless custom CSS is written this will appear in the top right of the page, next to the title', + storage=FileSystemStorage(**settings.UPLOADED_IMAGES), + sizes={ + 'poster': 515, + 'flyer': (270, 165), + 'featured': (200, 130), + 'thumb': (126, 77), + }) + + def __unicode__(self): + return self.name + + def get_absolute_url(self): + return reverse('group', kwargs={'slug': self.slug}) + + @property + def projects(self): + return self.members.all().order_by('groupmembership__order') + + +class GroupMembership(models.Model): + project = models.ForeignKey(Project) + group = models.ForeignKey(Group) + order = models.IntegerField(max_length=2, default=1, + help_text="Order can also be set on the listing page once saved") + + class ProjectStep(models.Model): project = models.ForeignKey('Project', related_name='steps') content = models.TextField() @@ -147,10 +199,10 @@ def name(self): def website(self): if self.partner: return self.partner.website - return none + return None @property def logo(self): if self.partner: return self.partner.logo - return None \ No newline at end of file + return None diff --git a/make_mozilla/projects/templates/projects/detail.html b/make_mozilla/projects/templates/projects/detail.html index e6d8fe4..f6fd404 100644 --- a/make_mozilla/projects/templates/projects/detail.html +++ b/make_mozilla/projects/templates/projects/detail.html @@ -29,6 +29,14 @@

About this project

{% endif %} {% endfor %} + {% if project.groups %} +

A project from:

+ + {% endif %} {% if project.contributor %} diff --git a/make_mozilla/projects/templates/projects/group_index.html b/make_mozilla/projects/templates/projects/group_index.html new file mode 100644 index 0000000..0e7e718 --- /dev/null +++ b/make_mozilla/projects/templates/projects/group_index.html @@ -0,0 +1,25 @@ +{% extends "projects/base.html" %} + +{% block page_title %}{{ group.name }} | {{ super() }}{% endblock %} + +{% block page_id %}{{ group.slug }}{% endblock %} +{% block section_type %}project-group{% endblock %} + +{% block content %} +
+

welcome to the {{ group.name }}

+ + {{ group.body|safe }} +
+
    + {% for p in group.projects %} +
  1. + + +

    {{ p }}

    +
    + {% if p.contributor %}

    Contributed by {{ p.contributor }}

    {% endif %} +
  2. + {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/make_mozilla/projects/templates/projects/groups/test_project.html b/make_mozilla/projects/templates/projects/groups/test_project.html new file mode 100644 index 0000000..2958f0a --- /dev/null +++ b/make_mozilla/projects/templates/projects/groups/test_project.html @@ -0,0 +1,25 @@ +{% extends "projects/base.html" %} + +{% block page_title %}{{ group.name }} | {{ super() }}{% endblock %} + +{% block page_id %}{{ group.slug }}{% endblock %} +{% block section_type %}project-group{% endblock %} + +{% block content %} +
+

welcome to the {{ group.name }}

+ +

I like cake

+
+
    + {% for p in group.projects %} +
  1. + + +

    {{ p }}

    +
    + {% if p.contributor %}

    Contributed by {{ p.contributor }}

    {% endif %} +
  2. + {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/make_mozilla/projects/templates/projects/index.html b/make_mozilla/projects/templates/projects/index.html index 30eff3d..0f6ab06 100644 --- a/make_mozilla/projects/templates/projects/index.html +++ b/make_mozilla/projects/templates/projects/index.html @@ -45,6 +45,13 @@

Webmaker Projects:

{{ project }}

+ {% if project.groups %} + + {% endif %} {% if project.contributor %}

Contributed by {{ project.contributor }}

{% endif %} {% endfor %} diff --git a/make_mozilla/projects/urls.py b/make_mozilla/projects/urls.py index 5600192..b128c74 100644 --- a/make_mozilla/projects/urls.py +++ b/make_mozilla/projects/urls.py @@ -2,6 +2,8 @@ from make_mozilla.projects import views urlpatterns = patterns('', + url(r'^groups/(?P[\w-]+)/$', + views.group, name='group'), url(r'^$', views.index, name='projects'), url(r'(?P[\w-]+)/$', diff --git a/make_mozilla/projects/views.py b/make_mozilla/projects/views.py index 3e787eb..23052b8 100644 --- a/make_mozilla/projects/views.py +++ b/make_mozilla/projects/views.py @@ -114,7 +114,7 @@ def __call__(self, request): def submit(request): - return jingo.render(request, 'projects/submit.html'); + return jingo.render(request, 'projects/submit.html') def details(request, slug): @@ -126,4 +126,17 @@ def details(request, slug): return jingo.render(request, 'projects/detail.html', { 'project': project - }); + }) + + +def group(request, slug): + group = get_object_or_404(models.Group, slug=slug) + + if group.take_body_from == 'temp': + template = 'projects/groups/%s' % group.body_text_template + else: + template = 'projects/group_index.html' + + return jingo.render(request, template, { + 'group': group + }) diff --git a/media/css/base.less b/media/css/base.less index 24943fc..5e4bd61 100644 --- a/media/css/base.less +++ b/media/css/base.less @@ -2371,12 +2371,39 @@ ul.tri-set img { } /* ---[ Project Flyers ]---------------------------------------------------- */ - +.group-intro { + padding: 0 350px 4em 0; + border-bottom: 1px dotted #D6D6D6; + margin-bottom: 4em; + overflow: hidden; + position: relative; +} +.group-intro h1 { + font-style: italic; + font-size: 48px; + overflow: hidden; + font-weight: normal; + letter-spacing: -3px; + line-height: 1.1; +} +.group-intro h1 em { + display: inline-block; + font-size: 70px; + text-transform: uppercase; + font-weight: bold; + font-style: normal; + color: #676767; +} +.group-intro img { + position: absolute; + top: 0; + right: 0; +} .project-flyer { .box-shadow(@panelShadow); - background: #FFF; padding: 8px; + position: relative; } .project-flyer img { @@ -2396,7 +2423,26 @@ ul.tri-set img { background: #F1F2F2; border-top: solid 1px #D7D8D9; } - +.project-flyer .project-group { + overflow: hidden; + background: yellow; + list-style-type: none; + padding: 0.5em 10px; + margin: 0; + position: absolute; + top: 0; + left: 0; +} +.project-flyer .project-group li { + padding: 0; + margin: 0; + width: auto; +} +.project-flyer .project-group em { + font-weight: bold; + text-decoration: underline; + font-style: normal; +} .project-flyer-list { margin: -3px 0 0 -20px; padding: 3px 0 0; @@ -2418,6 +2464,15 @@ ul.tri-set img { } @media screen and (max-width: 700px) { + .group-intro { + padding-right: 0; + } + .group-intro img { + position: static; + float: right; + width: 50%; + height: auto; + } .project-flyer-list { padding-right: 5px; margin-right: -5px;