Merge remote-tracking branch 'origin/master' into landing-page
merge with latest
This commit is contained in:
commit
4acf500bc8
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -68,6 +68,8 @@ class Show(TimestampedModel):
|
|||||||
help_text="A banner used for the show's page.",
|
help_text="A banner used for the show's page.",
|
||||||
verbose_name="Artwork"
|
verbose_name="Artwork"
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return '%s [%s]'%(self.name,self.abbr)
|
||||||
|
|
||||||
class User(TimestampedModel):
|
class User(TimestampedModel):
|
||||||
user_id = models.CharField(
|
user_id = models.CharField(
|
||||||
@ -92,6 +94,8 @@ class User(TimestampedModel):
|
|||||||
related_name='watched_by',
|
related_name='watched_by',
|
||||||
through='Watch'
|
through='Watch'
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return self.email
|
||||||
|
|
||||||
class Admin(User):
|
class Admin(User):
|
||||||
pass
|
pass
|
||||||
@ -136,6 +140,8 @@ class Ban(TimestampedModel):
|
|||||||
help_text='If checked, this is a site-wide ban, and the user is automatically banned from all shows, not just those in the Banned From (scope) paramenter',
|
help_text='If checked, this is a site-wide ban, and the user is automatically banned from all shows, not just those in the Banned From (scope) paramenter',
|
||||||
verbose_name = 'Site Wide Ban'
|
verbose_name = 'Site Wide Ban'
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return ("Permanent" if self.permanent else "Temporary") + " ban of %s"%self.user
|
||||||
|
|
||||||
class ShowModerator(TimestampedModel):
|
class ShowModerator(TimestampedModel):
|
||||||
show = models.ForeignKey(
|
show = models.ForeignKey(
|
||||||
@ -160,6 +166,8 @@ class ShowModerator(TimestampedModel):
|
|||||||
help_text='The user who appointed this moderator',
|
help_text='The user who appointed this moderator',
|
||||||
verbose_name='Appointed by'
|
verbose_name='Appointed by'
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return "%s on %s"%(self.user,self.show.abbr)
|
||||||
|
|
||||||
class Report(TimestampedModel):
|
class Report(TimestampedModel):
|
||||||
reporter = models.ForeignKey(
|
reporter = models.ForeignKey(
|
||||||
@ -184,6 +192,8 @@ class Report(TimestampedModel):
|
|||||||
help_text='The URL of the content being reported',
|
help_text='The URL of the content being reported',
|
||||||
verbose_name = 'Content URL'
|
verbose_name = 'Content URL'
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return "%s's report of %s"%(self.reporter, self.url)
|
||||||
|
|
||||||
class ShowSubmission(TimestampedModel):
|
class ShowSubmission(TimestampedModel):
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
@ -193,11 +203,17 @@ class ShowSubmission(TimestampedModel):
|
|||||||
help_text='The user who submitted this show',
|
help_text='The user who submitted this show',
|
||||||
verbose_name='Submitter'
|
verbose_name='Submitter'
|
||||||
)
|
)
|
||||||
name = Show.name
|
name = models.CharField(
|
||||||
|
max_length=40,
|
||||||
|
help_text="The full name of the show",
|
||||||
|
verbose_name="Full Name"
|
||||||
|
)
|
||||||
details = models.TextField(
|
details = models.TextField(
|
||||||
help_text='Some details about the show. Why it should be added, where information about it can be found, etc.',
|
help_text='Some details about the show. Why it should be added, where information about it can be found, etc.',
|
||||||
verbose_name='Details'
|
verbose_name='Details'
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return '"%s" by %s'%(self.name, self.user)
|
||||||
|
|
||||||
class Season(models.Model):
|
class Season(models.Model):
|
||||||
show = models.ForeignKey(
|
show = models.ForeignKey(
|
||||||
@ -225,6 +241,8 @@ class Season(models.Model):
|
|||||||
verbose_name="Artwork",
|
verbose_name="Artwork",
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return self.show.name + " S%d"%self.number
|
||||||
|
|
||||||
class Episode(models.Model):
|
class Episode(models.Model):
|
||||||
show = models.ForeignKey(
|
show = models.ForeignKey(
|
||||||
@ -246,7 +264,7 @@ class Episode(models.Model):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=40,
|
max_length=40,
|
||||||
help_text='The name given to this episode by its producers',
|
help_text='The name given to this episode by its producers',
|
||||||
verbose_name='Episode Season'
|
verbose_name='Episode Name'
|
||||||
)
|
)
|
||||||
summary = models.TextField(
|
summary = models.TextField(
|
||||||
help_text='A summary of this episode'
|
help_text='A summary of this episode'
|
||||||
@ -255,6 +273,8 @@ class Episode(models.Model):
|
|||||||
help_text='The date this episode officially aired for the first time',
|
help_text='The date this episode officially aired for the first time',
|
||||||
verbose_name='Original Air Date'
|
verbose_name='Original Air Date'
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return "[s%dep%d] %s — %s"%(self.season.number,self.episode,self.show.name, self.name)
|
||||||
|
|
||||||
class Submission(TimestampedModel):
|
class Submission(TimestampedModel):
|
||||||
episode = models.ForeignKey(
|
episode = models.ForeignKey(
|
||||||
@ -278,6 +298,8 @@ class Submission(TimestampedModel):
|
|||||||
help_text='Tags applied to this link submission',
|
help_text='Tags applied to this link submission',
|
||||||
max_length=200
|
max_length=200
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return '%s\'s submission for %s — s%dep%d'%(self.user,self.episode.show.name,self.episode.season.number, self.episode.episode)
|
||||||
|
|
||||||
class SubmissionVote(TimestampedModel):
|
class SubmissionVote(TimestampedModel):
|
||||||
submission = models.ForeignKey(
|
submission = models.ForeignKey(
|
||||||
@ -295,6 +317,8 @@ class SubmissionVote(TimestampedModel):
|
|||||||
positive = models.BooleanField(
|
positive = models.BooleanField(
|
||||||
help_text='If this is true, the vote is an upvote. Otherwise, it is a downvote'
|
help_text='If this is true, the vote is an upvote. Otherwise, it is a downvote'
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return "%s's vote on %s"%(self.user,self.submission)
|
||||||
|
|
||||||
class Favorite(TimestampedModel):
|
class Favorite(TimestampedModel):
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
@ -305,6 +329,8 @@ class Favorite(TimestampedModel):
|
|||||||
Episode,
|
Episode,
|
||||||
on_delete=models.CASCADE
|
on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return "%s \u2665 %s"%(self.user, self.episode)
|
||||||
|
|
||||||
class Watch(TimestampedModel):
|
class Watch(TimestampedModel):
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
@ -315,6 +341,8 @@ class Watch(TimestampedModel):
|
|||||||
Episode,
|
Episode,
|
||||||
on_delete=models.CASCADE
|
on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return "%s \U0001f441 %s"%(self.user, self.episode)
|
||||||
|
|
||||||
class DiscussionBoard(TimestampedModel):
|
class DiscussionBoard(TimestampedModel):
|
||||||
show = models.ForeignKey(
|
show = models.ForeignKey(
|
||||||
@ -338,6 +366,8 @@ class DiscussionBoard(TimestampedModel):
|
|||||||
help_text='The body of the post',
|
help_text='The body of the post',
|
||||||
verbose_name='Body'
|
verbose_name='Body'
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return '[%s] "%s" by %s'%(self.show.abbr, self.title, self.user)
|
||||||
|
|
||||||
class DiscussionReply(TimestampedModel):
|
class DiscussionReply(TimestampedModel):
|
||||||
board = models.ForeignKey(
|
board = models.ForeignKey(
|
||||||
@ -357,6 +387,8 @@ class DiscussionReply(TimestampedModel):
|
|||||||
help_text='The body of the response',
|
help_text='The body of the response',
|
||||||
verbose_name='Body'
|
verbose_name='Body'
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return '[%s] %s\'s response to "%s"'%(self.board.show.abbr,self.user, self.board.title)
|
||||||
|
|
||||||
class DiscussionVote(TimestampedModel):
|
class DiscussionVote(TimestampedModel):
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
@ -371,6 +403,8 @@ class DiscussionVote(TimestampedModel):
|
|||||||
related_name='votes',
|
related_name='votes',
|
||||||
help_text='The board this vote was cast on'
|
help_text='The board this vote was cast on'
|
||||||
)
|
)
|
||||||
postive = models.BooleanField(
|
positive = models.BooleanField(
|
||||||
help_text='If true, the vote is an upvote. Otherwise, it is a downvote. Neutral votes are not recorded'
|
help_text='If true, the vote is an upvote. Otherwise, it is a downvote. Neutral votes are not recorded'
|
||||||
)
|
)
|
||||||
|
def __str__(self):
|
||||||
|
return "%s %s %s"%(self.user, '\U0001f592' if self.positive else '\U0001f44e', self.board.title)
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -55,6 +55,7 @@ class LoginRedirect(View):
|
|||||||
).json()
|
).json()
|
||||||
req.session['user_id'] = user_info['uuid']
|
req.session['user_id'] = user_info['uuid']
|
||||||
matches = User.objects.filter(user_id=user_info['uuid'])
|
matches = User.objects.filter(user_id=user_info['uuid'])
|
||||||
|
match = None
|
||||||
if not len(matches):
|
if not len(matches):
|
||||||
user = User(
|
user = User(
|
||||||
user_id = user_info['uuid'],
|
user_id = user_info['uuid'],
|
||||||
@ -62,7 +63,11 @@ class LoginRedirect(View):
|
|||||||
display_name = user_info['display_name']
|
display_name = user_info['display_name']
|
||||||
)
|
)
|
||||||
user.save()
|
user.save()
|
||||||
|
match = user
|
||||||
|
else:
|
||||||
|
match = matches[0]
|
||||||
req.session['token'] = resp_json['access_token']
|
req.session['token'] = resp_json['access_token']
|
||||||
|
req.session['disp_name'] = match.display_name
|
||||||
return HttpResponseRedirect('/')
|
return HttpResponseRedirect('/')
|
||||||
else:
|
else:
|
||||||
return HttpResponse('<h1>Unmatching state tokens</h1><br><p>It looks like the request to login wasn\'t started by you. Try going back to the home page and logging in again.</p>', status=400)
|
return HttpResponse('<h1>Unmatching state tokens</h1><br><p>It looks like the request to login wasn\'t started by you. Try going back to the home page and logging in again.</p>', status=400)
|
||||||
|
56
README.md
Normal file
56
README.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Episodes.Community
|
||||||
|
## Overview
|
||||||
|
Episodes.Community is an in-development website for the discussion and viewing of any and every television show. An index of episodes is maintained for each show. Users are able to submit a link to a streaming location for an episode. This link is then voted on by other users, with the resulting score determining that link's priority.
|
||||||
|
|
||||||
|
## Planned Features
|
||||||
|
* Each show given a subdomain that can be used as an alternative to the url
|
||||||
|
* Tags can be set by users on link submissions, and links can be filtered by tag
|
||||||
|
* A system by which content can be flagged by users for admin/moderators to check
|
||||||
|
* User moderators for shows
|
||||||
|
* An api for automation of link submission
|
||||||
|
|
||||||
|
See a detailed draft [here](https://docs.google.com/document/d/1VI-wDvCF-3qvyC7tVX0HwruII93UDUJvFjTIfrH4fZI/edit?usp=sharing)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
0. Install prerequisites
|
||||||
|
* [python3](https://www.python.org/)
|
||||||
|
* [pip](https://pip.pypa.io/en/stable/installing/) (or manually install all python deps in the [requirements][requirements.txt] file)
|
||||||
|
* Some kind of database server. Any kind [supported by Django](https://docs.djangoproject.com/en/1.11/ref/databases/) will work. You can use a third-party database as well, but you are responsible for [configuring Django](https://docs.djangoproject.com/en/1.11/ref/databases/#using-a-3rd-party-database-backend), and should not expect help from the community.
|
||||||
|
* [gunicorn](http://gunicorn.org/) (for production)
|
||||||
|
1. Clone the repository
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/IcyNet/IcyNet.eu.git
|
||||||
|
$ cd IcyNet.eu
|
||||||
|
```
|
||||||
|
2. Install requirements. If you're using pip (recomended), use
|
||||||
|
```
|
||||||
|
$ sudo pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
if installing as root, or
|
||||||
|
```
|
||||||
|
$ pip install -r requirements.txt --user
|
||||||
|
```
|
||||||
|
if installing for your user only.
|
||||||
|
3. If you're not using a full release, you'll need to generate the migration instructions. If you are using a full release, you can skip this step.
|
||||||
|
```
|
||||||
|
$ python3 manage.py makemigrations
|
||||||
|
```
|
||||||
|
4. Copy the config file, and make any needed changes
|
||||||
|
```
|
||||||
|
$ cp options_example.ini options.ini
|
||||||
|
$ $EDITOR options.ini
|
||||||
|
```
|
||||||
|
5. Setup the database
|
||||||
|
```
|
||||||
|
$ python3 manage.py migrate
|
||||||
|
```
|
||||||
|
6. Run the server. For development purposes, you can use
|
||||||
|
```
|
||||||
|
$ python3 manage.py runserver
|
||||||
|
```
|
||||||
|
For production, run
|
||||||
|
```
|
||||||
|
$ gunicorn EpisodesCommunity.wsgi
|
||||||
|
```
|
||||||
|
## Contributing
|
||||||
|
If you want to contribute, we'd love your help. You can get in contact with @LunaSquee or @Tsa6, or just start in on anything on the [issues list](https://github.com/IcyNet/Episodes.Community/issues) that hasn't already been assigned. We do ask that you follow the [GitHub Workflow](https://guides.github.com/introduction/flow/)
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
0
tests/LandingPage/__init__.py
Normal file
0
tests/LandingPage/__init__.py
Normal file
96
tests/LandingPage/test_views.py
Normal file
96
tests/LandingPage/test_views.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
from django.test import TestCase,Client,override_settings
|
||||||
|
import responses
|
||||||
|
from LandingPage.models import User
|
||||||
|
from urllib import parse
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
AUTH_TOKEN_ENDPOINT='http://icynet.test/api/',
|
||||||
|
AUTH_CLIENT_ID='clid',
|
||||||
|
AUTH_B64='Y2xpZDpjbGlzZWM=',
|
||||||
|
AUTH_REDIRECT_URL='http://redirect.test'
|
||||||
|
)
|
||||||
|
class TestLogin(TestCase):
|
||||||
|
|
||||||
|
def test_login_new_user(self):
|
||||||
|
# Set up responses to control network flow
|
||||||
|
with responses.RequestsMock() as rm:
|
||||||
|
rm.add(responses.POST,'http://icynet.test/api/token',json={'access_token':'1accesstoken1'})
|
||||||
|
rm.add(responses.GET,'http://icynet.test/api/user',json={'uuid':'935a41b5-b38d-42c3-96ef-653402fc44ca','email':'johnsmith@gmail.com','display_name':'Mr. Smith'})
|
||||||
|
|
||||||
|
# Make initial request to redirect endpoint
|
||||||
|
client = Client()
|
||||||
|
resp = client.get('/login')
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
query = parse.parse_qs(parse.urlparse(resp['Location']).query)
|
||||||
|
state = query['state'][0]
|
||||||
|
self.assertEqual(query['client_id'][0],'clid')
|
||||||
|
self.assertEqual(query['response_type'][0],'code')
|
||||||
|
self.assertEqual(query['redirect_uri'][0],'http://redirect.test')
|
||||||
|
self.assertEqual(query['scope'][0],'email')
|
||||||
|
|
||||||
|
# Make connection to the real endpoint
|
||||||
|
resp = client.get('/login/redirect?state=%s&code=%s'%(state, 'code'))
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
|
||||||
|
# Check that the database is all good
|
||||||
|
users = User.objects.all()
|
||||||
|
self.assertEqual(len(users), 1)
|
||||||
|
user = users[0]
|
||||||
|
self.assertEqual(user.user_id,'935a41b5-b38d-42c3-96ef-653402fc44ca')
|
||||||
|
self.assertEqual(user.email,'johnsmith@gmail.com')
|
||||||
|
self.assertEqual(user.display_name, 'Mr. Smith')
|
||||||
|
|
||||||
|
# Check appropriate values are in the session
|
||||||
|
self.assertEqual(client.session['user_id'], '935a41b5-b38d-42c3-96ef-653402fc44ca')
|
||||||
|
self.assertEqual(client.session['token'],'1accesstoken1')
|
||||||
|
self.assertEqual(client.session['disp_name'], 'Mr. Smith')
|
||||||
|
|
||||||
|
def test_reject_bad_state(self):
|
||||||
|
with responses.RequestsMock() as rm:
|
||||||
|
client = Client()
|
||||||
|
resp = client.get('/login/redirect?state=%s&code=%s'%('bad_state', 'code'))
|
||||||
|
self.assertEqual(resp.status_code, 400)
|
||||||
|
|
||||||
|
def test_login_old_user(self):
|
||||||
|
# Set up responses to control network flow
|
||||||
|
with responses.RequestsMock() as rm:
|
||||||
|
rm.add(responses.POST,'http://icynet.test/api/token',json={'access_token':'1accesstoken1'})
|
||||||
|
rm.add(responses.GET,'http://icynet.test/api/user',json={'uuid':'935a41b5-b38d-42c3-96ef-653402fc44ca','email':'johnsmith@gmail.com','display_name':'Mr. Smith'})
|
||||||
|
|
||||||
|
# Set up the database
|
||||||
|
user = User(user_id='935a41b5-b38d-42c3-96ef-653402fc44ca',email='johnsmith@gmail.com',display_name='Mr. Smith')
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
# Make initial request to redirect endpoint
|
||||||
|
client = Client()
|
||||||
|
resp = client.get('/login')
|
||||||
|
state = parse.parse_qs(parse.urlparse(resp['Location']).query)['state'][0]
|
||||||
|
|
||||||
|
# Make connection to the real endpoint
|
||||||
|
resp = client.get('/login/redirect?state=%s&code=%s'%(state, 'code'))
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
|
||||||
|
# Check that the database is all good
|
||||||
|
users = User.objects.all()
|
||||||
|
self.assertEqual(len(users), 1)
|
||||||
|
user = users[0]
|
||||||
|
self.assertEqual(user.user_id,'935a41b5-b38d-42c3-96ef-653402fc44ca')
|
||||||
|
self.assertEqual(user.email,'johnsmith@gmail.com')
|
||||||
|
self.assertEqual(user.display_name, 'Mr. Smith')
|
||||||
|
|
||||||
|
# Check appropriate values are in the session
|
||||||
|
self.assertEqual(client.session['user_id'], '935a41b5-b38d-42c3-96ef-653402fc44ca')
|
||||||
|
self.assertEqual(client.session['token'],'1accesstoken1')
|
||||||
|
self.assertEqual(client.session['disp_name'], 'Mr. Smith')
|
||||||
|
|
||||||
|
def test_states_unique(self):
|
||||||
|
with responses.RequestsMock() as rm:
|
||||||
|
client1 = Client()
|
||||||
|
resp1 = client1.get('/login')
|
||||||
|
state1 = parse.parse_qs(parse.urlparse(resp1['Location']).query)['state'][0]
|
||||||
|
|
||||||
|
client2 = Client()
|
||||||
|
resp2 = client2.get('/login')
|
||||||
|
state2 = parse.parse_qs(parse.urlparse(resp2['Location']).query)['state'][0]
|
||||||
|
|
||||||
|
self.assertNotEqual(state1,state2)
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
Reference in New Issue
Block a user