Ver código fonte

Added canvas models (merge --squash)

Harshvardhan Pandit 6 anos atrás
pai
commit
10bda0b652

+ 47 - 3
.gitignore

@@ -1,4 +1,3 @@
-# ---> Python
 # Byte-compiled / optimized / DLL files
 __pycache__/
 *.py[cod]
@@ -9,7 +8,6 @@ __pycache__/
 
 # Distribution / packaging
 .Python
-env/
 build/
 develop-eggs/
 dist/
@@ -21,9 +19,11 @@ lib64/
 parts/
 sdist/
 var/
+wheels/
 *.egg-info/
 .installed.cfg
 *.egg
+MANIFEST
 
 # PyInstaller
 #  Usually these files are written by a python script from a template
@@ -43,7 +43,9 @@ htmlcov/
 .cache
 nosetests.xml
 coverage.xml
-*,cover
+*.cover
+.hypothesis/
+.pytest_cache/
 
 # Translations
 *.mo
@@ -51,6 +53,15 @@ coverage.xml
 
 # Django stuff:
 *.log
+local_settings.py
+#db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
 
 # Sphinx documentation
 docs/_build/
@@ -58,3 +69,36 @@ docs/_build/
 # PyBuilder
 target/
 
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/

+ 0 - 0
canvas2/canvas2/__init__.py


+ 121 - 0
canvas2/canvas2/settings.py

@@ -0,0 +1,121 @@
+"""
+Django settings for canvas2 project.
+
+Generated by 'django-admin startproject' using Django 2.0.3.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.0/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/2.0/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'jb4sx*xt^9#u7e(7ycgkle(7ya*l=cm1^04pw0pg-tqu15+c8)'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'catalog.apps.CatalogConfig'
+]
+
+MIDDLEWARE = [
+    'django.middleware.security.SecurityMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'canvas2.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': ['./templates'],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = 'canvas2.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+    }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+    },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/2.0/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'Europe/Dublin'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/2.0/howto/static-files/
+
+STATIC_URL = '/static/'

+ 25 - 0
canvas2/canvas2/urls.py

@@ -0,0 +1,25 @@
+"""canvas2 URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+    https://docs.djangoproject.com/en/2.0/topics/http/urls/
+Examples:
+Function views
+    1. Add an import:  from my_app import views
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
+Class-based views
+    1. Add an import:  from other_app.views import Home
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
+Including another URLconf
+    1. Import the include() function: from django.urls import include, path
+    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import path, include
+from django.views.generic import RedirectView
+
+urlpatterns = [
+    path('admin/', admin.site.urls),
+    path('catalog/', include('catalog.urls')),
+    path('', RedirectView.as_view(url='/catalog/')),
+    path('accounts/', include('django.contrib.auth.urls')),
+]

+ 16 - 0
canvas2/canvas2/wsgi.py

@@ -0,0 +1,16 @@
+"""
+WSGI config for canvas2 project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "canvas2.settings")
+
+application = get_wsgi_application()

+ 0 - 0
canvas2/catalog/__init__.py


+ 11 - 0
canvas2/catalog/admin.py

@@ -0,0 +1,11 @@
+from django.contrib import admin
+from .models import Canvas, IdeaCategory, CanvasTag, Idea, Comment
+
+
+admin.site.register(Canvas)
+admin.site.register(IdeaCategory)
+admin.site.register(CanvasTag)
+admin.site.register(Idea)
+admin.site.register(Comment)
+
+# Register your models here.

+ 5 - 0
canvas2/catalog/apps.py

@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class CatalogConfig(AppConfig):
+    name = 'catalog'

+ 1 - 0
canvas2/catalog/changes.txt

@@ -0,0 +1 @@
+TODOs completed. removed collaborations list in canvas_list.html, as the table for collaborations was deleted. 

+ 43 - 0
canvas2/catalog/forms.py

@@ -0,0 +1,43 @@
+from django import forms
+
+from django.contrib.auth.models import User
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
+
+
+class SignUpForm(forms.Form):
+  '''
+  Each field (both passwords at once in their case) is checked with it's own function as 
+  by doing so all can be checked at once for errors and have errors thrown simultaneously.
+  If the user happens to enter an email that already exists as well as a username that already
+  exists and the two passwords don't match, then the user will be notified of all three errors
+  without having to perform three correct_error - submit - read_error cycles
+  '''
+  name = forms.CharField()
+  email = forms.EmailField()
+  password = forms.CharField(max_length = 50, min_length = 8, widget = forms.PasswordInput)
+  password2 = forms.CharField(max_length = 50, min_length = 8, widget = forms.PasswordInput)
+
+   
+  # clean passwords, two fields can't be declared as above 
+  def clean(self):
+    cleanName = self.cleaned_data['name']
+    nameExists = User.objects.filter(username = cleanName).count()
+
+    if nameExists > 0 :
+      raise ValidationError(_('Username already exists. Please try a different one.'))
+
+    mail = self.cleaned_data['email']
+    emailExists = User.objects.filter(email = mail).count()
+    
+    if emailExists > 0 :
+      raise ValidationError(_('An account already exists with the email provided. Please use a different email address, or log in.'))
+
+    p1 = self.cleaned_data.get('password')
+    p2 = self.cleaned_data.get('password2')
+
+    if (p1 != p2):
+      raise ValidationError(_('Passwords do not match.'))
+
+    return self.cleaned_data
+

+ 86 - 0
canvas2/catalog/migrations/0001_initial.py

@@ -0,0 +1,86 @@
+# Generated by Django 2.0.3 on 2018-04-04 15:39
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Canvas',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('title', models.CharField(db_index=True, help_text='The title of the canvas', max_length=255)),
+                ('date_created', models.DateTimeField(auto_now_add=True, db_index=True)),
+                ('date_modified', models.DateTimeField(auto_now=True, db_index=True)),
+                ('public', models.BooleanField(db_index=True, default=False)),
+                ('admins', models.ManyToManyField(related_name='admins', to=settings.AUTH_USER_MODEL)),
+                ('users', models.ManyToManyField(related_name='users', to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ('date_modified',),
+            },
+        ),
+        migrations.CreateModel(
+            name='CanvasTag',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('text', models.CharField(max_length=255)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Comment',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('text', models.CharField(help_text='Type a comment', max_length=255)),
+                ('resolved', models.BooleanField(db_index=True, default=False)),
+            ],
+            options={
+                'ordering': ('resolved',),
+            },
+        ),
+        migrations.CreateModel(
+            name='Idea',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('text', models.CharField(help_text='The description of the idea', max_length=255)),
+                ('date_created', models.DateTimeField(auto_now_add=True, db_index=True)),
+                ('date_modified', models.DateTimeField(auto_now=True, db_index=True)),
+                ('canvas', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='catalog.Canvas')),
+                ('canvas_tags', models.ManyToManyField(blank=True, related_name='canvas_tags', to='catalog.CanvasTag')),
+            ],
+            options={
+                'ordering': ('date_created',),
+            },
+        ),
+        migrations.CreateModel(
+            name='IdeaCategory',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('description', models.CharField(help_text='Category Description', max_length=50)),
+            ],
+        ),
+        migrations.AddField(
+            model_name='idea',
+            name='category',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='catalog.IdeaCategory'),
+        ),
+        migrations.AddField(
+            model_name='comment',
+            name='idea',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='catalog.Idea'),
+        ),
+        migrations.AddField(
+            model_name='comment',
+            name='user',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+        ),
+    ]

+ 0 - 0
canvas2/catalog/migrations/__init__.py


+ 118 - 0
canvas2/catalog/models.py

@@ -0,0 +1,118 @@
+from django.db import models
+from django.urls import reverse
+from django.contrib.auth.models import User
+from django.db.models.signals import pre_save
+from django.dispatch import receiver
+
+from django.utils import timezone
+import datetime
+
+
+# TODO: Add Idea categories
+# these are columns in the ethics canvas
+
+# TODO: Add Idea ordering (preserve the order of ideas shown)
+
+# TODO: CanvasTag instead of IdeaTag
+# canvases have tags and text within ideas is highlighted with these tags
+
+
+class Canvas(models.Model):
+    """
+    The model for canvas metadata
+    """
+    title = models.CharField(
+        max_length = 255, 
+        db_index = True,
+        help_text="The title of the canvas"
+    )
+    date_created = models.DateTimeField(auto_now_add = True, db_index = True)
+    date_modified = models.DateTimeField(auto_now = True, db_index = True)
+    public = models.BooleanField(default = False, db_index = True)
+    
+    admins = models.ManyToManyField(User, related_name = 'admins')
+    users = models.ManyToManyField(User, related_name = 'users')
+
+
+    def __str__(self):
+        """
+        String of the Canvas
+        """
+        return self.title
+
+    def get_absolute_url(self):
+        return reverse('canvas-detail', args = [str(self.pk)])
+
+    class Meta:
+        ordering = ('date_modified',)
+
+@staticmethod
+@receiver(pre_save, sender=Canvas)
+def ensure_canvas_has_atleast_one_admin(sender, instance, **kwargs):
+    if instance.admins.count == 0:
+        raise Exception('Canvas should have at least one admin.')
+        
+
+class IdeaCategory(models.Model):
+    """
+    Model associating ideas with a category
+    """
+    description = models.CharField(max_length = 50, help_text = "Category Description")
+
+    def __str__(self):
+        return self.description
+
+
+class CanvasTag(models.Model):
+    """
+    Model representing tags that relate ideas - many to many relationship, there may exist many different tags to an idea and vice versa 
+    Composite key made of the ideaID foreign key and the tagID 
+    """
+    text = models.CharField(max_length = 255)
+    
+    def __str__(self):
+        return self.text
+
+
+class Idea(models.Model):
+    """
+    Model representing an Idea on the canvas
+    """
+    # limit on charfields is 255
+    text = models.CharField(max_length = 255, help_text = "The description of the idea")
+    date_created = models.DateTimeField(auto_now_add = True, db_index = True)
+    date_modified = models.DateTimeField(auto_now = True, db_index = True)
+    
+    category = models.ForeignKey('IdeaCategory', on_delete = models.CASCADE, null = True)
+    canvas = models.ForeignKey('Canvas', on_delete = models.CASCADE, null = True)
+    canvas_tags = models.ManyToManyField('CanvasTag', related_name = 'canvas_tags', blank = True)
+
+
+    def __str__(self):
+        return self.text    
+
+    # for now, order by created in ascending order (oldest at top)
+    class Meta:
+        ordering = ('date_created',)
+
+
+class Comment(models.Model):
+    """
+    Model representing comments, 1-1 relationship as a single comment does not apply do different ideas 
+    """
+    text = models.CharField(max_length = 255, help_text = "Type a comment")
+    resolved = models.BooleanField(default = False, db_index = True)
+
+    user = models.ForeignKey(User, on_delete = models.CASCADE)
+    idea = models.ForeignKey('Idea', on_delete = models.CASCADE, null = True)
+
+    def __str__(self):
+        if isResolved == true: 
+            return 'Comment: ' + self.text + '\n STATUS: Resolved'
+        else:
+            return 'Comment: ' + self.text + '\n STATUS: Unresolved'
+
+    class Meta: 
+        ordering = ('resolved',)
+
+

Diferenças do arquivo suprimidas por serem muito extensas
+ 34 - 0
canvas2/catalog/static/canvas2/images/online-ethics-canvas.pdf


+ 19 - 0
canvas2/catalog/templates/base_generic.html

@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+  {% block title %}<title>Ethics Canvas</title>{% endblock %}
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <style>
+    * {
+      font-size: 10px;
+      color: black;
+    }
+  </style>
+</head>
+<body>
+  {% block content %}<p>Fucking hell it works but there's nothing here</p>{% endblock %}
+
+
+</body>
+</html>

+ 17 - 0
canvas2/catalog/templates/catalog/canvas_detail.html

@@ -0,0 +1,17 @@
+{% extends "base_generic.html" %}
+
+{% block title %}
+<title>Canvas Title: {{ canvas.title }} </title>
+{% endblock %}
+
+{% block content %}
+  {% if idea_list %}
+  <ul>
+    {% for idea in idea_list %}
+      <li> {{ idea.text }} </li>
+    {% endfor %}
+  </ul>
+  {% else %}
+    <p>No ideas</p>
+  {% endif %}
+{% endblock %}

+ 39 - 0
canvas2/catalog/templates/catalog/canvas_list.html

@@ -0,0 +1,39 @@
+{% extends "base_generic.html" %}
+
+
+{% block title %}
+  <h1>Welcome {{ user.username }}.</h1>
+{% endblock %}
+
+
+{% block content %}
+  {% if private_canvas_list %}
+    <h2>Your canvasses are listed below.</h2>
+    <ul>
+    {% for canvas in private_canvas_list %}
+      <li>
+        <a href="{{ canvas.get_absolute_url }}">{{ canvas.title }}</a>
+      </li>
+    {% endfor %}
+    </ul>
+  {% else %}
+    <p>You have no canvasses. Click <a href="">here</a> to create one.</p>
+  {% endif %}
+
+  {% if public_canvas_list %}
+  <h2>Public Canvasses listed below.</h2>
+    <ul> 
+    {% for canvas in public_canvas_list %}
+      <li>
+        <a href="{{ canvas.get_absolute_url }}">{{ canvas.title }}</a>
+      </li>
+    {% endfor %}
+    </ul>
+  {% endif %}
+
+  <!-- On logout, redirect to the landing page -->
+  <a href="{% url 'new-canvas' %}">New Canvas</a>
+  <a href="{% url 'logout' %}?next=/catalog/">Log Out</a>
+
+{% endblock %}
+

+ 4 - 0
canvas2/catalog/templates/catalog/new_canvas.html

@@ -0,0 +1,4 @@
+{% extends "base_generic.html" %}
+{% block content %}
+<p> hi </p>
+{% endblock %}

+ 12 - 0
canvas2/catalog/templates/catalog/register.html

@@ -0,0 +1,12 @@
+{% extends "base_generic.html" %} 
+
+{% block content %}
+  <p> HERP </p>
+  <form action = "" method = "post">
+    {% csrf_token %}
+    <table>
+    {{ form }}
+    </table>
+    <input type = "submit" value = "Sign Up"/>
+  </form>
+{% endblock %}

+ 21 - 0
canvas2/catalog/templates/index.html

@@ -0,0 +1,21 @@
+{% extends "base_generic.html" %}
+
+{% block content %}
+  <p>Index</p>
+  <div class="buttons">
+    <ul>
+      <li>
+        <a href="">Try Online</a>
+      </li>
+      <li>
+        <a href="{% url 'login' %}">Log In</a>
+      </li>
+      <li>
+        <a href="{% url 'register' %}">Register</a>
+      </li>
+      <li>
+        <a href="/static/canvas2/images/online-ethics-canvas.pdf" download>Download Blank PDF</a>
+      </li>
+    </ul>
+  </div>
+{% endblock %}

+ 0 - 0
canvas2/catalog/tests/__init__.py


+ 234 - 0
canvas2/catalog/tests/test_models.py

@@ -0,0 +1,234 @@
+'''
+from django.test import TestCase
+from django.utils import timezone
+from catalog.models import Canvas, IdeaCategory, IdeaTag, Idea, Comment
+from django.contrib.auth.models import User
+import pytz
+import datetime
+
+# CANVAS IDEA USER COMMENT TAG COLLABORATIONS
+# Tests include 10 instances of any model being tested
+# ID is queried with i+1, as i begins at zero but IDs begin with 1 (and as it turns out cannot be 0)
+
+global TEST_LIMIT
+TEST_LIMIT = 10
+
+
+
+class CanvasTestModel(TestCase):
+  """
+
+  """
+  @classmethod
+  def setUpTestData(cls):
+    for i in range(TEST_LIMIT):
+      Canvas.objects.create(title = 'Hella Derp %s' % i, date = ( timezone.now() - datetime.timedelta(days = i) ) )
+
+  def testTitle(self):
+    for i in range(TEST_LIMIT):
+      canvas = Canvas.objects.get(id=(i+1))
+      field_label = canvas.title
+      self.assertEquals(field_label, 'Hella Derp %s' % i)
+
+  def testDate(self):
+    for i in range(TEST_LIMIT):
+      canvas = Canvas.objects.get(id=(i+1))
+      field_label = canvas.date
+      self.assertTrue( ( timezone.now() - datetime.timedelta(days = i) ).date() == field_label )
+
+
+
+class IdeaTestModel(TestCase):
+  """
+
+  """
+  @classmethod
+  def setUpTestData(cls):
+    for i in range(TEST_LIMIT):
+      User.objects.create_user( email = 'fatclub%s@example.com' % i, username = 'Fatclub %s' % i)
+      Idea.objects.create( ownerID = User.objects.get(id = i+1),text = 'I am an idea and my name is %s' % (i+1))
+
+
+  def testTextIsRight(self):
+    for i in range(TEST_LIMIT):
+      idea = Idea.objects.get(id=(i+1))
+      self.assertEquals(idea.text, 'I am an idea and my name is %s' % (i+1))
+
+  def testID(self):
+    for i in range(TEST_LIMIT):
+      idea = Idea.objects.get(id=(i+1))
+      self.assertEquals(idea.id, (i+1))
+
+  def testLinkedUser(self):
+    for i in range(TEST_LIMIT):
+      idea = Idea.objects.get(id=(i+1))
+      user = idea.ownerID
+      self.assertEquals( user.username, 'Fatclub %s' % i )
+      self.assertEquals( user.email, 'fatclub%s@example.com' % i )
+
+
+
+class CollaborationsTestModel(TestCase):
+  """
+
+  """
+  @classmethod
+  def setUpTestData(cls):
+    for i in range(TEST_LIMIT):
+      # First create the user and the canvas instances
+      User.objects.create_user( email = 'fatclub%s@example.com' % i, username = 'Fatclub %s' % i)
+      Canvas.objects.create(title = 'Hella Derp %s' % i, date = (timezone.now() - datetime.timedelta(days = i)) )
+      # Then create the collaborations instance and assign the two prior instances to it
+      Collaborations.objects.create(canvasID = Canvas.objects.get(id = (i+1)), userID = User.objects.get(id = (i+1)) )
+
+  def testIDs(self):
+    for i in range(TEST_LIMIT):
+      collab = Collaborations.objects.get(id = (i+1))
+      self.assertEquals(collab.canvasID, Canvas.objects.get(id = (i+1)))
+      self.assertEquals(collab.userID, User.objects.get(id = (i+1)))
+
+  def testLinkedUsers(self):
+    for i in range(TEST_LIMIT):
+      collab = Collaborations.objects.get(id = (i+1))
+      # as it turns out the user instance itself is acquired by assigning from the userID field - the instance is stored here, not the numeric ID 
+      user = collab.userID
+      self.assertEquals(user.email, 'fatclub%s@example.com' % i)
+      self.assertEquals(user.username, 'Fatclub %s' % i)
+
+  def testLinkedCanvasses(self):
+    for i in range(TEST_LIMIT):
+      collab = Collaborations.objects.get(id = (i+1))
+      canvas = collab.canvasID
+      self.assertEquals(canvas.title, 'Hella Derp %s' % i)
+      self.assertTrue( (timezone.now() - datetime.timedelta(days = i)).date() == canvas.date )
+
+
+
+class CommentsTestModel(TestCase):
+  """
+
+  """
+  @classmethod
+  def setUpTestData(cls):
+    for i in range(TEST_LIMIT * 2):
+      # ID Sequence: 1,3,5,7,9,11,13,15,17,19
+      User.objects.create_user( email = 'fatclub%s@example.com' % i, username = 'Fatclub %s' % i)   # ODD
+      # ID Sequence: 2,4,6,8,10,12,14,16,18,20
+      User.objects.create_user( email = 'commenter%s@example.com' % i, username = 'Commenter %s' % i) # EVEN
+    
+    for i in range(TEST_LIMIT):
+      # ID Sequence: 1,3,5,7,9,11,13,15,17,19
+      Idea.objects.create( ownerID = User.objects.get(id = i*2 + 1),text = 'I am an idea and my name is %s' % (i+1)) 
+      # ID Sequence: 2,4,6,8,10,12,14,16,18,20
+      Comments.objects.create( ownerID = User.objects.get(id = i * 2 + 2), ideaID = Idea.objects.get(id = (i+1)), text = "ok but why though, evil boss %s" %i )
+
+  def testInitiallyUnresolved(self):
+    for i in range(TEST_LIMIT):
+      comment = Comments.objects.get(id = i+1)
+      self.assertFalse(comment.isResolved)
+
+  def testLinkedIdea(self):
+    for i in range(TEST_LIMIT):
+      comment = Comments.objects.get(id = i+1)
+      idea = comment.ideaID
+      self.assertEquals(idea.text, 'I am an idea and my name is %s' % (i+1))
+
+  def testCommenter(self):
+    for i in range(TEST_LIMIT):
+      comment = Comments.objects.get(id = i+1)
+      user = comment.ownerID
+      self.assertEquals( user.username, 'Commenter %s' % i )
+      self.assertEquals( user.email, 'commenter%s@example.com' % i )
+
+  def testOwnerOfLinkedIdea(self):
+    for i in range(TEST_LIMIT):
+      idea = Idea.objects.get(id=(i+1))
+      user = idea.ownerID
+      self.assertEquals( user.username, 'Fatclub %s' % i )
+      self.assertEquals( user.email, 'fatclub%s@example.com' % i )
+'''
+
+'''
+TAGS MODEL CHANGED - TESTS NO LONGER VALID
+
+
+
+class TagListDoublingTestModel(TestCase):
+  """
+  """
+  @classmethod
+  def setUpTestData(cls):
+    for i in range (TEST_LIMIT * 2):
+      # 20 ideas, 2:1 idea:tag ratio - only text wanted for purposes of the test
+      Idea.objects.create(text = 'I am an idea and my name is %s' % (i+1)) 
+    for i in range (TEST_LIMIT):
+      # Tag ID range 1,1,2,2,3,3,4,4,5,5
+      # Compound ID (1,1)--(1,2) (2,3)--(2,4) (3,5)--(3,6) (4,7)--(4,8) (5,9)--(5,10)
+      Tags.objects.create(ideaID = Idea.objects.get(id = i + 1))
+
+  def testIdeaPairing(self):
+    for i in range(TEST_LIMIT):
+      # only test i values 0,2,4,6,8 as for odd values the tests will fail due to them starting too high for the given tagID
+      if (i % 2 == 0):
+        tag = Tags.objects.all()
+        idea0 = tag[0].ideaID
+        idea1 = tag[1].ideaID
+        self.assertEquals(idea0.text, 'I am an idea and my name is %s' % (i+1))
+        self.assertEquals(idea1.text, 'I am an idea and my name is %s' % (i+2))
+
+
+
+
+
+class TagListTriplingTestModel(TestCase):
+  """
+  """
+  @classmethod
+  def setUpTestData(cls):
+    for i in range (TEST_LIMIT * 3):
+      # 30 ideas, 3:1 idea:tag ratio - only text wanted for purposes of the test
+      Idea.objects.create(text = 'I am an idea and my name is %s' % (i+1)) 
+    for i in range (12):
+      # Tag ID range 1,1,1,2,2,2,3,3,3,4,4,4
+      # Compound ID (1,1)--(1,2)--(1,3) (2,4)--(2,5)--(2,6) (3,7)--(3,8)--(3,9) (4,10)--(4,11)--(4,12)
+      Tags.objects.create(ideaID = Idea.objects.get(id = i + 1))
+
+  def testIdeaTripling(self):
+    for i in range(12):
+    # only test i values 0,3,6,9 as for other values the tests will fail due to them starting too high for the given tagID
+      if (i%3 == 0):
+        tag = Tags.objects.all()
+        idea0 = tag[0].ideaID
+        idea1 = tag[1].ideaID
+        idea2 = tag[2].ideaID
+        self.assertEquals(idea0.text, 'I am an idea and my name is %s' % (i+1))
+        self.assertEquals(idea1.text, 'I am an idea and my name is %s' % (i+2))
+        self.assertEquals(idea2.text, 'I am an idea and my name is %s' % (i+3))
+
+
+
+
+
+class TagListManyTagsSingleIdea(TestCase):
+  """
+  """
+  @classmethod
+  def setUpTestData(cls):
+    for i in range (TEST_LIMIT):
+      Idea.objects.create(text = 'I am an idea and my name is %s' % (i+1)) 
+      Tags.objects.create(Idea.tags = Idea.objects.get(id = 1))
+
+  def testIdeaTripling(self):
+    for i in range(TEST_LIMIT):
+      tag = Tags.objects.all()
+      # above query returns a singleton list of tag objects for some reason, which is distinct from a tag object
+      idea = tag[0].Idea.tags
+      self.assertEquals(idea.text, 'I am an idea and my name is %s' % (1))
+
+
+
+'''
+
+      
+
+

+ 15 - 0
canvas2/catalog/urls.py

@@ -0,0 +1,15 @@
+from django.urls import path
+from django.contrib.auth import views as auth_views
+from . import views
+
+
+urlpatterns = [
+  path('', views.index, name='index'),
+  path('canvas-list/', views.CanvasList.as_view(), name='canvas-list'),
+  path('canvas/<int:pk>/', views.CanvasDetailView.as_view(), name='canvas-detail'),
+  path('register/', views.register, name='register'),
+  path('new_canvas/', views.new_canvas, name='new-canvas'),
+  path('accounts/login/', auth_views.LoginView.as_view(template_name='registration/login.html')),
+  path('accounts/logout/', auth_views.LogoutView.as_view(template_name='registration/logout.html')),
+
+]

+ 107 - 0
canvas2/catalog/views.py

@@ -0,0 +1,107 @@
+from django.shortcuts import render, redirect
+from django.views import generic
+from django.views.generic.edit import CreateView, UpdateView, DeleteView
+from django.http import HttpResponseRedirect
+from django.urls import reverse, reverse_lazy
+from django.contrib.auth.models import User
+from django.contrib.auth import authenticate, login
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.contrib.auth.decorators import login_required
+
+from .models import Canvas, IdeaCategory, CanvasTag, Idea, Comment
+from .forms import SignUpForm
+
+# TODO: update views based on models
+
+# TODO: use django.utils.timezone instead of datetime
+import django.utils.timezone 
+
+@login_required
+def new_canvas(request):
+    canvas = Canvas(title = "New Canvas", public = False)
+    canvas.save()
+    
+    canvas.admins.add(request.user)
+    canvas.users.add(request.user)
+    canvas.save()
+
+    return redirect(canvas.get_absolute_url()) # bring user to the canvas page for the newly created canvas
+
+
+def index(request):
+    return render(request, 'index.html')
+
+def register(request):
+    if request.method == 'POST':
+        form = SignUpForm(request.POST)
+
+        if form.is_valid():
+              username = form.cleaned_data['name']
+              email = form.cleaned_data['email']
+              password = form.cleaned_data['password']
+
+              newUser = User.objects.create_user(username = username, email = email, password = password)
+
+              return HttpResponseRedirect(reverse('index'))
+      
+    else:
+        form = SignUpForm(initial = {
+            'name': '',
+            'email': '',
+            'password': '',
+            'password2': '',
+        })
+
+
+    return render(
+        request,
+        'catalog/register.html',
+        {'form': form}
+    )
+
+
+class CanvasList(LoginRequiredMixin, generic.ListView):
+    # TODO: update this based on changes in Model
+    model = Canvas
+
+    def get_context_data(self, **kwargs):
+        '''
+        This function's purpose is to separate the public from the private canvasses
+        '''
+        me = self.request.user
+        context = super().get_context_data(**kwargs)
+
+        filter_kwargs = {'public': True}
+#        me_filter_kwargs = {'users': me}
+
+        canvasses = Canvas.objects.all()
+
+        # public canvasses are those where public is true
+        public = canvasses.filter(**filter_kwargs)
+        # private canvasses where the user is either the owner or a collaborator on the canvas
+        private = canvasses.exclude(**filter_kwargs).filter(users__pk = me)
+
+        context['public_canvas_list'] = public
+        context['private_canvas_list'] = private
+
+        return context
+
+
+class CanvasDetailView(LoginRequiredMixin, generic.DetailView):
+    model = Canvas
+
+    def get_context_data(self, **kwargs):
+        '''
+        This function is to return a set of ideas where their canvasID attribute matches the current canvas id. 
+        '''
+        context = super().get_context_data(**kwargs)
+        contextCID = (context['canvas'].pk)
+        filter_kwargs = {'canvas': contextCID}
+
+        ideas = Idea.objects.all()
+        filtered_ideas = ideas.filter(**filter_kwargs)
+        context['idea_list'] = filtered_ideas
+
+        return context
+
+

BIN
canvas2/db.sqlite3


+ 15 - 0
canvas2/manage.py

@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "canvas2.settings")
+    try:
+        from django.core.management import execute_from_command_line
+    except ImportError as exc:
+        raise ImportError(
+            "Couldn't import Django. Are you sure it's installed and "
+            "available on your PYTHONPATH environment variable? Did you "
+            "forget to activate a virtual environment?"
+        ) from exc
+    execute_from_command_line(sys.argv)

+ 41 - 0
canvas2/templates/registration/login.html

@@ -0,0 +1,41 @@
+{% extends "base_generic.html" %}
+
+<!--
+  Below code is from the django documentation website
+-->
+{% block content %}
+
+{% if form.errors %}
+<p>Your username and password didn't match. Please try again.</p>
+{% endif %}
+
+{% if next %}
+    {% if user.is_authenticated %}
+    <p>Your account doesn't have access to this page. To proceed,
+    please login with an account that has access.</p>
+    {% else %}
+    <p>Please login to see this page.</p>
+    {% endif %}
+{% endif %}
+
+<form method="post" action="{% url 'login' %}">
+{% csrf_token %}
+<table>
+<tr>
+    <td>{{ form.username.label_tag }}</td>
+    <td>{{ form.username }}</td>
+</tr>
+<tr>
+    <td>{{ form.password.label_tag }}</td>
+    <td>{{ form.password }}</td>
+</tr>
+</table>
+
+<input type="submit" value="login" />
+<input type="hidden" name="next" value="{% url 'canvas-list' %}" />
+</form>
+
+{# Assumes you setup the password_reset view in your URLconf #}
+<!--<p><a href="{% url 'password_reset' %}">Lost password?</a></p>-->
+
+{% endblock %}

+ 2 - 0
requirements.txt

@@ -0,0 +1,2 @@
+python 3.6.4
+django 2.0.3

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff