From b10a2df51b3a5c0832b1550c5a42932051d2057a Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Fri, 15 Dec 2017 23:41:35 -0700 Subject: [PATCH 1/9] #13 project database model, controller, preliminary UI --- acmwebsite/controllers/project.py | 32 +++++++++++++ acmwebsite/controllers/root.py | 2 + acmwebsite/model/__init__.py | 14 +++++- acmwebsite/model/auth.py | 1 + acmwebsite/model/project.py | 33 +++++++++++++ acmwebsite/templates/master.xhtml | 1 + acmwebsite/templates/project.xhtml | 14 ++++++ acmwebsite/templates/projects.xhtml | 40 ++++++++++++++++ migration/versions/9ecde33126dc_meetings.py | 14 +++--- migration/versions/d0578a57e0f9_projects.py | 52 +++++++++++++++++++++ 10 files changed, 196 insertions(+), 7 deletions(-) create mode 100644 acmwebsite/controllers/project.py create mode 100644 acmwebsite/model/project.py create mode 100644 acmwebsite/templates/project.xhtml create mode 100644 acmwebsite/templates/projects.xhtml create mode 100644 migration/versions/d0578a57e0f9_projects.py diff --git a/acmwebsite/controllers/project.py b/acmwebsite/controllers/project.py new file mode 100644 index 0000000..cadecdc --- /dev/null +++ b/acmwebsite/controllers/project.py @@ -0,0 +1,32 @@ +"""Project controller module.""" + +from tg import expose, abort + +from acmwebsite.lib.base import BaseController +from acmwebsite.model import DBSession, Project + + +class ProjectController(BaseController): + def __init__(self, project): + self.project = project + + @expose('acmwebsite.templates.project') + def index(self): + return dict(page='project', project=self.project) + + +class ProjectsController(BaseController): + """Root controller for listing all projects""" + def __init__(self): + self.projects = DBSession.query(Project) + + @expose() + def _lookup(self, id, *args): + project = DBSession.query(Project).filter(Project.id == id).first() + if not project: + abort(404, "No such project") + return ProjectController(project), args + + @expose('acmwebsite.templates.projects') + def index(self): + return dict(page='projects', projects=self.projects) diff --git a/acmwebsite/controllers/root.py b/acmwebsite/controllers/root.py index 22da865..f71123c 100644 --- a/acmwebsite/controllers/root.py +++ b/acmwebsite/controllers/root.py @@ -22,6 +22,7 @@ from acmwebsite.controllers.meeting import MeetingsController from acmwebsite.controllers.schedule import ScheduleController from acmwebsite.controllers.survey import SurveysController +from acmwebsite.controllers.project import ProjectsController import datetime @@ -49,6 +50,7 @@ class RootController(BaseController): schedule = ScheduleController() error = ErrorController() contact = ContactController() + projects = ProjectsController() def _before(self, *args, **kw): tmpl_context.project_name = "acmwebsite" diff --git a/acmwebsite/model/__init__.py b/acmwebsite/model/__init__.py index 7035e11..7112ef4 100644 --- a/acmwebsite/model/__init__.py +++ b/acmwebsite/model/__init__.py @@ -62,5 +62,17 @@ def init_model(engine): from acmwebsite.model.meeting import Meeting from acmwebsite.model.mailmessage import MailMessage from acmwebsite.model.survey import Survey, SurveyField, SurveyResponse, SurveyData +from acmwebsite.model.project import Project -__all__ = ('User', 'Group', 'Permission', 'Meeting', 'MailMessage', 'Survey', 'SurveyField', 'SurveyResponse', 'SurveyData') +__all__ = ( + 'User', + 'Group', + 'Permission', + 'Meeting', + 'MailMessage', + 'Survey', + 'SurveyField', + 'SurveyResponse', + 'SurveyData', + 'Project', +) diff --git a/acmwebsite/model/auth.py b/acmwebsite/model/auth.py index 0f0ffc1..c7577c1 100644 --- a/acmwebsite/model/auth.py +++ b/acmwebsite/model/auth.py @@ -95,6 +95,7 @@ class User(DeclarativeBase): created = Column(DateTime, default=datetime.now) officer_title = Column(Unicode(255), nullable=True) profile_pic = Column(UploadedFileField) + projects = relation('Project', secondary='team', back_populates='team_members') def __repr__(self): return '' % ( diff --git a/acmwebsite/model/project.py b/acmwebsite/model/project.py new file mode 100644 index 0000000..9d67458 --- /dev/null +++ b/acmwebsite/model/project.py @@ -0,0 +1,33 @@ +"""Project model module.""" + +from sqlalchemy.orm import relationship +from sqlalchemy import Table, ForeignKey, Column, UniqueConstraint +from sqlalchemy.types import Integer, Unicode + +from depot.fields.sqlalchemy import UploadedFileField +from depot.fields.specialized.image import UploadedImageWithThumb + +from acmwebsite.model import DeclarativeBase, metadata, User + +team_table = Table( + 'team', + metadata, + Column('user_id', Integer(), ForeignKey('tg_user.user_id'), nullable=False), + Column('project_id', Integer(), ForeignKey('projects.id'), nullable=False), + UniqueConstraint('user_id', 'project_id'), +) + + +class Project(DeclarativeBase): + __tablename__ = 'projects' + + # Fields + id = Column(Integer, autoincrement=True, primary_key=True) + team_members = relationship(User, secondary=team_table, back_populates='projects') + name = Column(Unicode(1024), unique=True, nullable=False) + description = Column(Unicode(4096)) + website = Column(Unicode(512)) + repository = Column(Unicode(512)) + status = Column(Unicode(20), nullable=False, default="u") + reject_reason = Column(Unicode(512)) + image = Column(UploadedFileField(upload_type=UploadedImageWithThumb)) diff --git a/acmwebsite/templates/master.xhtml b/acmwebsite/templates/master.xhtml index f15b15d..7415f0a 100644 --- a/acmwebsite/templates/master.xhtml +++ b/acmwebsite/templates/master.xhtml @@ -35,6 +35,7 @@ diff --git a/acmwebsite/templates/project.xhtml b/acmwebsite/templates/project.xhtml new file mode 100644 index 0000000..7483bca --- /dev/null +++ b/acmwebsite/templates/project.xhtml @@ -0,0 +1,14 @@ + + + + + + Mines ACM - ${project.name} + + + +

+ + diff --git a/acmwebsite/templates/projects.xhtml b/acmwebsite/templates/projects.xhtml new file mode 100644 index 0000000..52c7cb9 --- /dev/null +++ b/acmwebsite/templates/projects.xhtml @@ -0,0 +1,40 @@ + + + + + + Mines ACM - Projects + + + +

Projects

+
+
+
+
+

+

+

+ ${h.icon('github')} + ${project.repository} +

+
+
+
+
+
+
+

Submit a Project

+
+
+

+ You must be logged in to submit a project +

+
+
+
+
+ + diff --git a/migration/versions/9ecde33126dc_meetings.py b/migration/versions/9ecde33126dc_meetings.py index da1ce90..dd2f430 100644 --- a/migration/versions/9ecde33126dc_meetings.py +++ b/migration/versions/9ecde33126dc_meetings.py @@ -13,15 +13,17 @@ from alembic import op import sqlalchemy as sa + def upgrade(): op.create_table( 'meeting', - sa.Column('id', sa.Integer(), primary_key=True), - sa.Column('date', sa.DateTime(), nullable=False), - sa.Column('location', sa.Text()), - sa.Column('title', sa.Unicode(), nullable=False), - sa.Column('description', sa.Unicode()) + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('date', sa.DateTime(), nullable=False), + sa.Column('location', sa.Text()), + sa.Column('title', sa.Unicode(), nullable=False), + sa.Column('description', sa.Unicode()) ) + def downgrade(): - op.drop_table('meeting') \ No newline at end of file + op.drop_table('meeting') diff --git a/migration/versions/d0578a57e0f9_projects.py b/migration/versions/d0578a57e0f9_projects.py new file mode 100644 index 0000000..9e8a02d --- /dev/null +++ b/migration/versions/d0578a57e0f9_projects.py @@ -0,0 +1,52 @@ +"""Projects Schema + +Revision ID: d0578a57e0f9 +Revises: 6ba4018c2666 +Create Date: 2017-12-15 22:15:07.284502 + +""" + +# revision identifiers, used by Alembic. +revision = 'd0578a57e0f9' +down_revision = '6ba4018c2666' + +from alembic import op +import sqlalchemy as sa + +from depot.fields.sqlalchemy import UploadedFileField +from depot.fields.specialized.image import UploadedImageWithThumb + + +def upgrade(): + # Add the Team XREF table + op.create_table( + 'team', + sa.Column('user_id', sa.Integer(), sa.ForeignKey('tg_user.user_id'), nullable=False), + sa.Column('project_id', sa.Integer(), sa.ForeignKey('projects.id'), nullable=False), + sa.UniqueConstraint('user_id', 'project_id'), + ) + + # Create the actual projects table + op.create_table( + 'projects', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('name', sa.Unicode(1024), nullable=False), + sa.Column('description', sa.Unicode(4096)), + sa.Column('website', sa.Unicode(512)), + sa.Column('repository', sa.Unicode(512)), + sa.Column('status', sa.Unicode(1), nullable=False, default="u"), + sa.Column('reject_reason', sa.Unicode(512)), + sa.Column('image', UploadedFileField(upload_type=UploadedImageWithThumb)), + + # Ensure that the statu is valid. + # u -> unverified, r-> rejected, v->verified + sa.CheckConstraint("status in ('u', 'r', 'v')"), + + # Require reject reason if project is rejected + sa.CheckConstraint("reject_reason is not null or status != 'r'") + ) + + +def downgrade(): + op.drop_table('team') + op.drop_table('projects') From 7d18779928792648abeab76c525f1f56b741e4ba Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Sat, 16 Dec 2017 01:52:43 -0700 Subject: [PATCH 2/9] #13 added pretty project display --- acmwebsite/controllers/project.py | 25 +++++++-- acmwebsite/lib/helpers.py | 4 ++ acmwebsite/public/css/style.css | 70 ++++++++++++++++--------- acmwebsite/templates/project_list.xhtml | 37 +++++++++++++ acmwebsite/templates/projects.xhtml | 17 +++--- 5 files changed, 115 insertions(+), 38 deletions(-) create mode 100644 acmwebsite/templates/project_list.xhtml diff --git a/acmwebsite/controllers/project.py b/acmwebsite/controllers/project.py index cadecdc..8c400ee 100644 --- a/acmwebsite/controllers/project.py +++ b/acmwebsite/controllers/project.py @@ -1,6 +1,7 @@ """Project controller module.""" -from tg import expose, abort +from tg import abort, expose, request +from tg.predicates import has_permission, not_anonymous from acmwebsite.lib.base import BaseController from acmwebsite.model import DBSession, Project @@ -17,16 +18,34 @@ def index(self): class ProjectsController(BaseController): """Root controller for listing all projects""" + def __init__(self): self.projects = DBSession.query(Project) @expose() def _lookup(self, id, *args): - project = DBSession.query(Project).filter(Project.id == id).first() + project = self.projects.filter(Project.id == id).first() if not project: abort(404, "No such project") return ProjectController(project), args @expose('acmwebsite.templates.projects') def index(self): - return dict(page='projects', projects=self.projects) + # Only show verified projects or projects that the current user is on. + if has_permission('admin'): + projects = self.projects.all() + elif request.identity: + user = request.identity['user'] + projects = self.projects.filter(Project.status == 'v' + or user in Project.team_members) + else: + projects = self.projects.filter(Project.status == 'v') + + return dict(page='projects', projects=projects) + + @expose('acmwebsite.templates.submit_project') + def submit(self): + if request.method == 'POST': + pass + else: + return dict(page='project_submit') diff --git a/acmwebsite/lib/helpers.py b/acmwebsite/lib/helpers.py index 9d5d457..6c6ab1a 100644 --- a/acmwebsite/lib/helpers.py +++ b/acmwebsite/lib/helpers.py @@ -29,6 +29,10 @@ def icon(icon_name): return Markup('' % icon_name) +def fa_icon(icon_name): + return Markup('' % icon_name) + + def ftime(datetime_obj, duration=None, show_day=False): day_fmt = '{0:%A}, ' if show_day else '' date_fmt = '{0.day} {0:%B %Y}' diff --git a/acmwebsite/public/css/style.css b/acmwebsite/public/css/style.css index 6e0ddd6..6cb673b 100644 --- a/acmwebsite/public/css/style.css +++ b/acmwebsite/public/css/style.css @@ -1,32 +1,32 @@ /* TurboGears flash bootstrap look */ #flash > div { - padding: 8px 35px 8px 14px; - margin-bottom: 20px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - background-color: #fcf8e3; - border: 1px solid #fbeed5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - color: #c09853; + padding: 8px 35px 8px 14px; + margin-bottom: 20px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + color: #c09853; } #flash > .ok { - color: #468847; - background-color: #dff0d8; - border-color: #d6e9c6; + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; } #flash > .error { - color: #b94a48; - background-color: #f2dede; - border-color: #eed3d7; + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; } #flash > .info { - color: #3a87ad; - background-color: #d9edf7; - border-color: #bce8f1; + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; } /* Footer styling */ @@ -55,14 +55,14 @@ } .navbar-right { - margin-right: 0px; + margin-right: 0; } /* Logo */ .acm-logo { - margin: 20px 0; - max-width: 400px; - width: 100%; + margin: 20px 0; + max-width: 400px; + width: 100%; } /* Contact Page */ @@ -106,10 +106,32 @@ /* Survey Page */ .radio-group .radio-option label, .checkbox-group .checkbox label { - font-weight: normal; + font-weight: normal; } /* Schedule Page */ .acm-meeting-title { - font-weight: bold; + font-weight: bold; +} + +/* Project Page */ +.project-list .row { + /* + * By default rows have -15px margins on the right and which doesn't line up + * correctly in a list-group-item. + */ + margin-left: 0; + margin-right: 0; +} + +.project-list .project-title { + margin-top: 10px; +} + +span.dot-sep { + padding: 0 5px; +} + +.project-list .thumbnail { + max-width: 250px; } diff --git a/acmwebsite/templates/project_list.xhtml b/acmwebsite/templates/project_list.xhtml new file mode 100644 index 0000000..82baa20 --- /dev/null +++ b/acmwebsite/templates/project_list.xhtml @@ -0,0 +1,37 @@ + diff --git a/acmwebsite/templates/projects.xhtml b/acmwebsite/templates/projects.xhtml index 52c7cb9..f6d765d 100644 --- a/acmwebsite/templates/projects.xhtml +++ b/acmwebsite/templates/projects.xhtml @@ -12,16 +12,7 @@

Projects

-
-
-

-

-

- ${h.icon('github')} - ${project.repository} -

-
-
+
@@ -30,7 +21,11 @@

- You must be logged in to submit a project + You must be logged in to submit a project. +

+

+ To have your project listed here, please + submit it for moderation.

From 2e7a7cfeab2d5d1fe371af7544fb46b262e80c55 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Sat, 16 Dec 2017 16:58:11 -0700 Subject: [PATCH 3/9] #13 Clean up a couple of minor things --- acmwebsite/controllers/project.py | 14 ++++++++------ acmwebsite/model/__init__.py | 3 ++- acmwebsite/templates/project_list.xhtml | 6 ++++-- acmwebsite/templates/projects.xhtml | 2 +- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/acmwebsite/controllers/project.py b/acmwebsite/controllers/project.py index 8c400ee..a91abe3 100644 --- a/acmwebsite/controllers/project.py +++ b/acmwebsite/controllers/project.py @@ -1,6 +1,6 @@ """Project controller module.""" -from tg import abort, expose, request +from tg import abort, expose, request, require from tg.predicates import has_permission, not_anonymous from acmwebsite.lib.base import BaseController @@ -34,18 +34,20 @@ def index(self): # Only show verified projects or projects that the current user is on. if has_permission('admin'): projects = self.projects.all() - elif request.identity: - user = request.identity['user'] - projects = self.projects.filter(Project.status == 'v' - or user in Project.team_members) + if request.identity: + uid = request.identity['user'].user_id + projects = [p for p in self.projects + if p.status == 'v' or uid in (u.user_id for u in p.team_members)] else: - projects = self.projects.filter(Project.status == 'v') + projects = self.projects.filter(Project.status == 'v').all() return dict(page='projects', projects=projects) @expose('acmwebsite.templates.submit_project') + @require(not_anonymous()) def submit(self): if request.method == 'POST': + # TODO: Sumner pass else: return dict(page='project_submit') diff --git a/acmwebsite/model/__init__.py b/acmwebsite/model/__init__.py index 7112ef4..6ba5aca 100644 --- a/acmwebsite/model/__init__.py +++ b/acmwebsite/model/__init__.py @@ -62,7 +62,7 @@ def init_model(engine): from acmwebsite.model.meeting import Meeting from acmwebsite.model.mailmessage import MailMessage from acmwebsite.model.survey import Survey, SurveyField, SurveyResponse, SurveyData -from acmwebsite.model.project import Project +from acmwebsite.model.project import Project, team_table __all__ = ( 'User', @@ -75,4 +75,5 @@ def init_model(engine): 'SurveyResponse', 'SurveyData', 'Project', + 'team_table', ) diff --git a/acmwebsite/templates/project_list.xhtml b/acmwebsite/templates/project_list.xhtml index 82baa20..97cedbb 100644 --- a/acmwebsite/templates/project_list.xhtml +++ b/acmwebsite/templates/project_list.xhtml @@ -1,7 +1,9 @@
-
- +
+
+ +
diff --git a/acmwebsite/templates/projects.xhtml b/acmwebsite/templates/projects.xhtml index f6d765d..976c3df 100644 --- a/acmwebsite/templates/projects.xhtml +++ b/acmwebsite/templates/projects.xhtml @@ -25,7 +25,7 @@

To have your project listed here, please - submit it for moderation. + submit it for moderation.

From 0dc5ebde5de997f123ef5f443a91b1f2fc8c1f98 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Sun, 17 Dec 2017 14:06:26 -0700 Subject: [PATCH 4/9] Add video URL field --- acmwebsite/controllers/project.py | 20 ++------------- acmwebsite/model/project.py | 1 + acmwebsite/public/css/style.css | 19 ++++++++++++++ acmwebsite/templates/project.xhtml | 14 ----------- acmwebsite/templates/project_list.xhtml | 28 +++++++++++---------- migration/versions/d0578a57e0f9_projects.py | 1 + 6 files changed, 38 insertions(+), 45 deletions(-) delete mode 100644 acmwebsite/templates/project.xhtml diff --git a/acmwebsite/controllers/project.py b/acmwebsite/controllers/project.py index a91abe3..6df279b 100644 --- a/acmwebsite/controllers/project.py +++ b/acmwebsite/controllers/project.py @@ -7,34 +7,18 @@ from acmwebsite.model import DBSession, Project -class ProjectController(BaseController): - def __init__(self, project): - self.project = project - - @expose('acmwebsite.templates.project') - def index(self): - return dict(page='project', project=self.project) - - class ProjectsController(BaseController): """Root controller for listing all projects""" def __init__(self): self.projects = DBSession.query(Project) - @expose() - def _lookup(self, id, *args): - project = self.projects.filter(Project.id == id).first() - if not project: - abort(404, "No such project") - return ProjectController(project), args - @expose('acmwebsite.templates.projects') def index(self): # Only show verified projects or projects that the current user is on. if has_permission('admin'): projects = self.projects.all() - if request.identity: + elif request.identity: uid = request.identity['user'].user_id projects = [p for p in self.projects if p.status == 'v' or uid in (u.user_id for u in p.team_members)] @@ -47,7 +31,7 @@ def index(self): @require(not_anonymous()) def submit(self): if request.method == 'POST': - # TODO: Sumner + # TODO (Sumner): this should create an unverified project, email mods? pass else: return dict(page='project_submit') diff --git a/acmwebsite/model/project.py b/acmwebsite/model/project.py index 9d67458..e74b7e8 100644 --- a/acmwebsite/model/project.py +++ b/acmwebsite/model/project.py @@ -28,6 +28,7 @@ class Project(DeclarativeBase): description = Column(Unicode(4096)) website = Column(Unicode(512)) repository = Column(Unicode(512)) + video_url = Column(Unicode(512)) status = Column(Unicode(20), nullable=False, default="u") reject_reason = Column(Unicode(512)) image = Column(UploadedFileField(upload_type=UploadedImageWithThumb)) diff --git a/acmwebsite/public/css/style.css b/acmwebsite/public/css/style.css index 6cb673b..4e90624 100644 --- a/acmwebsite/public/css/style.css +++ b/acmwebsite/public/css/style.css @@ -135,3 +135,22 @@ span.dot-sep { .project-list .thumbnail { max-width: 250px; } + +/* Add a dot between every project link */ +.project-links { + padding-left: 0; +} + +.project-links li { + display: inline-block; +} + +.project-links li:not(:last-child) { + margin-right: 5px; +} + +.project-links li:not(:last-child)::after { + content: '\00B7'; + font-size: 20px; + margin-left: 5px; +} diff --git a/acmwebsite/templates/project.xhtml b/acmwebsite/templates/project.xhtml deleted file mode 100644 index 7483bca..0000000 --- a/acmwebsite/templates/project.xhtml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - Mines ACM - ${project.name} - - - -

- - diff --git a/acmwebsite/templates/project_list.xhtml b/acmwebsite/templates/project_list.xhtml index 97cedbb..431da1c 100644 --- a/acmwebsite/templates/project_list.xhtml +++ b/acmwebsite/templates/project_list.xhtml @@ -6,27 +6,29 @@

+

+ -

-

- - ${h.fa_icon('code-fork')} - Repository - - - - ${h.fa_icon('globe')} - Website - -

+

- Team Members: + Contributors: