Compare commits
10 commits
024ef59428
...
3bbd987e35
Author | SHA1 | Date | |
---|---|---|---|
3bbd987e35 | |||
54ae2c7b06 | |||
b39fbaa402 | |||
770f4f6c26 | |||
a2f38653fb | |||
c5289f39bb | |||
1a5441ba75 | |||
6636119200 | |||
02efd52c48 | |||
5479785cc1 |
73 changed files with 379 additions and 223 deletions
12
README.md
12
README.md
|
@ -1,5 +1,9 @@
|
||||||
# Software Freedom Conservancy website
|
# Software Freedom Conservancy website
|
||||||
|
|
||||||
|
This is a Python/[Django](https://www.djangoproject.com/)-based website that
|
||||||
|
runs [sfconservancy.org](https://sfconservancy.org).
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
The canonical location for this repository is [on Conservancy’s
|
The canonical location for this repository is [on Conservancy’s
|
||||||
|
@ -9,9 +13,9 @@ Kallithea instance](https://k.sfconservancy.org/website).
|
||||||
## License
|
## License
|
||||||
|
|
||||||
The software included herein, such as the Python source files, are generally
|
The software included herein, such as the Python source files, are generally
|
||||||
licensed [AGPLv3](AGPLv3)-or-later. The Javascript is a hodgepodge of
|
licensed [AGPLv3](AGPLv3)-or-later. JavaScript source is generally
|
||||||
licensing, but all of it is compatible with [AGPLv3](AGPLv3)-or-later. See
|
[GPLv3](GPLv3)-or-later. See the notices at the top of each Javascript file for
|
||||||
the notices at the top of each Javascript file for licensing details.
|
specific licensing details.
|
||||||
|
|
||||||
The content and text (such as the HTML files) is currently
|
The content and text (such as the HTML files) is currently
|
||||||
[CC-BY-SA-3.0](CC-By-SA-3.0).
|
[CC-BY-SA-3.0](CC-By-SA-3.0).
|
||||||
|
@ -20,7 +24,7 @@ The content and text (such as the HTML files) is currently
|
||||||
## Server configuration
|
## Server configuration
|
||||||
|
|
||||||
Conservancy's webserver runs on a standard Debian installation. For
|
Conservancy's webserver runs on a standard Debian installation. For
|
||||||
configuration requirements, see `deploy/ansible/install.yml`.
|
configuration requirements, see `deploy/install.yml`.
|
||||||
|
|
||||||
|
|
||||||
## CDN
|
## CDN
|
||||||
|
|
6
bin/test
Executable file
6
bin/test
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
set -e # Abort on failure
|
||||||
|
set -x
|
||||||
|
|
||||||
|
python3 -m pytest
|
|
@ -17,5 +17,3 @@ class EntryAdmin(admin.ModelAdmin):
|
||||||
search_fields = ['headline', 'summary', 'body']
|
search_fields = ['headline', 'summary', 'body']
|
||||||
prepopulated_fields = {'slug': ("headline",)}
|
prepopulated_fields = {'slug': ("headline",)}
|
||||||
filter_horizontal = ('tags',)
|
filter_horizontal = ('tags',)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -72,11 +72,11 @@ class Entry(models.Model, bsoup.SoupModelMixin):
|
||||||
|
|
||||||
# Ping Technorati
|
# Ping Technorati
|
||||||
j = xmlrpc.client.Server('http://rpc.technorati.com/rpc/ping')
|
j = xmlrpc.client.Server('http://rpc.technorati.com/rpc/ping')
|
||||||
reply = j.weblogUpdates.ping(blog_name, blog_url)
|
j.weblogUpdates.ping(blog_name, blog_url)
|
||||||
|
|
||||||
# Ping Google Blog Search
|
# Ping Google Blog Search
|
||||||
j = xmlrpc.client.Server('http://blogsearch.google.com/ping/RPC2')
|
j = xmlrpc.client.Server('http://blogsearch.google.com/ping/RPC2')
|
||||||
reply = j.weblogUpdates.ping(blog_name, blog_url, post_url)
|
j.weblogUpdates.ping(blog_name, blog_url, post_url)
|
||||||
|
|
||||||
# Call any superclass's method
|
# Call any superclass's method
|
||||||
super().save()
|
super().save()
|
||||||
|
|
|
@ -3,7 +3,6 @@ from functools import reduce
|
||||||
|
|
||||||
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.views.generic import ListView
|
|
||||||
from django.views.generic.dates import (
|
from django.views.generic.dates import (
|
||||||
DateDetailView,
|
DateDetailView,
|
||||||
DayArchiveView,
|
DayArchiveView,
|
||||||
|
@ -12,7 +11,7 @@ from django.views.generic.dates import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..staff.models import Person
|
from ..staff.models import Person
|
||||||
from .models import Entry, EntryTag
|
from .models import EntryTag
|
||||||
|
|
||||||
|
|
||||||
def OR_filter(field_name, objs):
|
def OR_filter(field_name, objs):
|
||||||
|
|
|
@ -46,10 +46,12 @@ def run(cmd, encoding=None, ok_exitcodes=frozenset([0]), **kwargs):
|
||||||
no_data = ''
|
no_data = ''
|
||||||
with contextlib.ExitStack() as exit_stack:
|
with contextlib.ExitStack() as exit_stack:
|
||||||
proc = exit_stack.enter_context(subprocess.Popen(cmd, **kwargs))
|
proc = exit_stack.enter_context(subprocess.Popen(cmd, **kwargs))
|
||||||
pipes = [exit_stack.enter_context(open(
|
pipes = [
|
||||||
getattr(proc, name).fileno(), mode, encoding=encoding, closefd=False))
|
exit_stack.enter_context(open(
|
||||||
for name in ['stdout', 'stderr']
|
getattr(proc, name).fileno(), mode, encoding=encoding, closefd=False))
|
||||||
if kwargs.get(name) is subprocess.PIPE]
|
for name in ['stdout', 'stderr']
|
||||||
|
if kwargs.get(name) is subprocess.PIPE
|
||||||
|
]
|
||||||
if pipes:
|
if pipes:
|
||||||
yield (proc, *pipes)
|
yield (proc, *pipes)
|
||||||
else:
|
else:
|
||||||
|
@ -88,6 +90,7 @@ class GitPath:
|
||||||
|
|
||||||
def _cache(orig_func):
|
def _cache(orig_func):
|
||||||
attr_name = '_cached_' + orig_func.__name__
|
attr_name = '_cached_' + orig_func.__name__
|
||||||
|
|
||||||
@functools.wraps(orig_func)
|
@functools.wraps(orig_func)
|
||||||
def cache_wrapper(self):
|
def cache_wrapper(self):
|
||||||
try:
|
try:
|
||||||
|
@ -353,4 +356,3 @@ def main(arglist=None, stdout=sys.stdout, stderr=sys.stderr):
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
exit(main())
|
exit(main())
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,3 @@ class EventAdmin(admin.ModelAdmin):
|
||||||
@admin.register(EventMedia)
|
@admin.register(EventMedia)
|
||||||
class EventMediaAdmin(admin.ModelAdmin):
|
class EventMediaAdmin(admin.ModelAdmin):
|
||||||
list_display = ("event", "format", "novel")
|
list_display = ("event", "format", "novel")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -96,4 +96,3 @@ class EventMedia(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{} media: {}".format(self.event, self.format)
|
return "{} media: {}".format(self.event, self.format)
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,11 @@ from .news.models import PressRelease
|
||||||
|
|
||||||
|
|
||||||
class ConservancyFeedBase(Feed):
|
class ConservancyFeedBase(Feed):
|
||||||
def copyright_holder(self): return "Software Freedom Conservancy"
|
def copyright_holder(self):
|
||||||
|
return "Software Freedom Conservancy"
|
||||||
|
|
||||||
def license_no_html(self): return "Licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License."
|
def license_no_html(self):
|
||||||
|
return "Licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License."
|
||||||
|
|
||||||
def item_copyright(self, item):
|
def item_copyright(self, item):
|
||||||
year = 2008
|
year = 2008
|
||||||
|
@ -89,7 +91,8 @@ class OmnibusFeed(ConservancyFeedBase):
|
||||||
def item_description(self, item):
|
def item_description(self, item):
|
||||||
return item.summary
|
return item.summary
|
||||||
|
|
||||||
def item_enclosure_mime_type(self): return "audio/mpeg"
|
def item_enclosure_mime_type(self):
|
||||||
|
return "audio/mpeg"
|
||||||
|
|
||||||
def item_enclosure_url(self, item):
|
def item_enclosure_url(self, item):
|
||||||
if hasattr(item, 'mp3_path'):
|
if hasattr(item, 'mp3_path'):
|
||||||
|
@ -98,9 +101,6 @@ class OmnibusFeed(ConservancyFeedBase):
|
||||||
if hasattr(item, 'mp3_path'):
|
if hasattr(item, 'mp3_path'):
|
||||||
return item.mp3_length
|
return item.mp3_length
|
||||||
|
|
||||||
def item_pubdate(self, item):
|
|
||||||
return item.pub_date
|
|
||||||
|
|
||||||
def item_author_name(self, item):
|
def item_author_name(self, item):
|
||||||
if item.omnibus_type == "blog":
|
if item.omnibus_type == "blog":
|
||||||
return item.author.formal_name
|
return item.author.formal_name
|
||||||
|
@ -174,7 +174,8 @@ class BlogFeed(ConservancyFeedBase):
|
||||||
firstTime = True
|
firstTime = True
|
||||||
done = {}
|
done = {}
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
if tag in done: continue
|
if tag in done:
|
||||||
|
continue
|
||||||
if firstTime:
|
if firstTime:
|
||||||
answer += " ("
|
answer += " ("
|
||||||
firstTime = False
|
firstTime = False
|
||||||
|
@ -192,8 +193,10 @@ class BlogFeed(ConservancyFeedBase):
|
||||||
|
|
||||||
GET = obj.GET
|
GET = obj.GET
|
||||||
tags = []
|
tags = []
|
||||||
if 'author' in GET: tags = GET.getlist('author')
|
if 'author' in GET:
|
||||||
if 'tag' in GET: tags += GET.getlist('tag')
|
tags = GET.getlist('author')
|
||||||
|
if 'tag' in GET:
|
||||||
|
tags += GET.getlist('tag')
|
||||||
|
|
||||||
done = {}
|
done = {}
|
||||||
if len(tags) == 1:
|
if len(tags) == 1:
|
||||||
|
@ -201,7 +204,8 @@ class BlogFeed(ConservancyFeedBase):
|
||||||
elif len(tags) > 1:
|
elif len(tags) > 1:
|
||||||
firstTime = True
|
firstTime = True
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
if tag in done: continue
|
if tag in done:
|
||||||
|
continue
|
||||||
if firstTime:
|
if firstTime:
|
||||||
answer += " tagged with "
|
answer += " tagged with "
|
||||||
firstTime = False
|
firstTime = False
|
||||||
|
|
|
@ -19,7 +19,3 @@ class ExternalArticleAdmin(admin.ModelAdmin):
|
||||||
list_filter = ['date']
|
list_filter = ['date']
|
||||||
date_hierarchy = 'date'
|
date_hierarchy = 'date'
|
||||||
search_fields = ["title", "info", "publication"]
|
search_fields = ["title", "info", "publication"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -34,8 +34,10 @@ class PressRelease(models.Model, bsoup.SoupModelMixin):
|
||||||
return self.headline
|
return self.headline
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return "/news/{}/{}/".format(self.pub_date.strftime("%Y/%b/%d").lower(),
|
return "/news/{}/{}/".format(
|
||||||
self.slug)
|
self.pub_date.strftime("%Y/%b/%d").lower(),
|
||||||
|
self.slug,
|
||||||
|
)
|
||||||
|
|
||||||
def is_recent(self):
|
def is_recent(self):
|
||||||
return self.pub_date > (datetime.now() - timedelta(days=5))
|
return self.pub_date > (datetime.now() - timedelta(days=5))
|
||||||
|
@ -60,11 +62,11 @@ class PressRelease(models.Model, bsoup.SoupModelMixin):
|
||||||
|
|
||||||
# Ping Technorati
|
# Ping Technorati
|
||||||
j = xmlrpc.client.Server('http://rpc.technorati.com/rpc/ping')
|
j = xmlrpc.client.Server('http://rpc.technorati.com/rpc/ping')
|
||||||
reply = j.weblogUpdates.ping(blog_name, blog_url)
|
j.weblogUpdates.ping(blog_name, blog_url)
|
||||||
|
|
||||||
# Ping Google Blog Search
|
# Ping Google Blog Search
|
||||||
j = xmlrpc.client.Server('http://blogsearch.google.com/ping/RPC2')
|
j = xmlrpc.client.Server('http://blogsearch.google.com/ping/RPC2')
|
||||||
reply = j.weblogUpdates.ping(blog_name, blog_url, post_url)
|
j.weblogUpdates.ping(blog_name, blog_url, post_url)
|
||||||
|
|
||||||
# Call any superclass's method
|
# Call any superclass's method
|
||||||
super().save()
|
super().save()
|
||||||
|
@ -114,4 +116,3 @@ class ExternalArticle(models.Model):
|
||||||
|
|
||||||
objects = models.Manager()
|
objects = models.Manager()
|
||||||
public = PublicExternalArticleManager()
|
public = PublicExternalArticleManager()
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
from django.views.generic.dates import (
|
from django.views.generic.dates import (
|
||||||
|
@ -11,8 +10,7 @@ from django.views.generic.dates import (
|
||||||
YearArchiveView,
|
YearArchiveView,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..events.models import Event
|
from .models import PressRelease
|
||||||
from .models import ExternalArticle, PressRelease
|
|
||||||
|
|
||||||
|
|
||||||
class NewsListView(ListView):
|
class NewsListView(ListView):
|
||||||
|
@ -27,8 +25,9 @@ def listing(request, *args, **kwargs):
|
||||||
news_queryset = PressRelease.objects.all()
|
news_queryset = PressRelease.objects.all()
|
||||||
|
|
||||||
# if (not kwargs.has_key('allow_future')) or not kwargs['allow_future']:
|
# if (not kwargs.has_key('allow_future')) or not kwargs['allow_future']:
|
||||||
news_queryset = news_queryset.filter(**{'%s__lte' % kwargs['date_field']:
|
news_queryset = news_queryset.filter(
|
||||||
datetime.now()})
|
**{'%s__lte' % kwargs['date_field']: datetime.now()}
|
||||||
|
)
|
||||||
|
|
||||||
date_list = news_queryset.dates(kwargs['date_field'], 'year')
|
date_list = news_queryset.dates(kwargs['date_field'], 'year')
|
||||||
|
|
||||||
|
@ -93,4 +92,3 @@ class NewsDateDetailView(DateDetailView):
|
||||||
# # del kwargs['slug']
|
# # del kwargs['slug']
|
||||||
# callable = NewsDateDetailView.as_view(**kwargs)
|
# callable = NewsDateDetailView.as_view(**kwargs)
|
||||||
# return callable(request)
|
# return callable(request)
|
||||||
|
|
||||||
|
|
|
@ -39,5 +39,3 @@ class CastAdmin(admin.ModelAdmin):
|
||||||
search_fields = ['title', 'summary', 'body']
|
search_fields = ['title', 'summary', 'body']
|
||||||
prepopulated_fields = {'slug': ("title",)}
|
prepopulated_fields = {'slug': ("title",)}
|
||||||
filter_horizontal = ('tags',)
|
filter_horizontal = ('tags',)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
|
|
||||||
class Podcast(models.Model):
|
class Podcast(models.Model):
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from .base import *
|
from .base import * # NOQA
|
||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
|
@ -2,7 +2,7 @@ import json
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
from .base import *
|
from .base import * # NOQA
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
ALLOWED_HOSTS = ['www.sfconservancy.org', 'sfconservancy.org']
|
ALLOWED_HOSTS = ['www.sfconservancy.org', 'sfconservancy.org']
|
||||||
|
@ -25,7 +25,7 @@ DATABASES = {
|
||||||
|
|
||||||
# Apache/mod_wsgi doesn't make it straightforward to pass environment variables
|
# Apache/mod_wsgi doesn't make it straightforward to pass environment variables
|
||||||
# to Django (can't use the Apache config).
|
# to Django (can't use the Apache config).
|
||||||
with open(BASE_DIR.parent / 'secrets.json') as f:
|
with open(BASE_DIR.parent / 'secrets.json') as f: # NOQA
|
||||||
secrets = json.load(f)
|
secrets = json.load(f)
|
||||||
|
|
||||||
def get_secret(secrets, setting):
|
def get_secret(secrets, setting):
|
||||||
|
|
|
@ -8,4 +8,3 @@ class PersonAdmin(admin.ModelAdmin):
|
||||||
list_display = ("username", "formal_name", "casual_name",
|
list_display = ("username", "formal_name", "casual_name",
|
||||||
"currently_employed")
|
"currently_employed")
|
||||||
list_filter = ["currently_employed"]
|
list_filter = ["currently_employed"]
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ p, h1, h2, h3, h4, h5, h6, #mainContent ul, #mainContent ol {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p, li {
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
1
conservancy/static/css/tachyons.min.css
vendored
1
conservancy/static/css/tachyons.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -4434,4 +4434,3 @@ being here with Ken thanks so much
|
||||||
|
|
||||||
01:00:28.380 --> 01:00:31.380
|
01:00:28.380 --> 01:00:31.380
|
||||||
foreign
|
foreign
|
||||||
|
|
||||||
|
|
|
@ -202,4 +202,3 @@ we request
|
||||||
00:03:38.680 --> 00:03:44.200
|
00:03:38.680 --> 00:03:44.200
|
||||||
that you grant Karen Sandler
|
that you grant Karen Sandler
|
||||||
the KU Leuven honorary doctorate.
|
the KU Leuven honorary doctorate.
|
||||||
|
|
||||||
|
|
|
@ -214,4 +214,3 @@ op voordracht van de Academische Raad,
|
||||||
00:03:38.680 --> 00:03:44.200
|
00:03:38.680 --> 00:03:44.200
|
||||||
het eredoctoraat van de KU Leuven
|
het eredoctoraat van de KU Leuven
|
||||||
te verlenen aan mevrouw Karen Sandler.
|
te verlenen aan mevrouw Karen Sandler.
|
||||||
|
|
||||||
|
|
|
@ -6,4 +6,3 @@ from .models import Supporter
|
||||||
@admin.register(Supporter)
|
@admin.register(Supporter)
|
||||||
class SupporterAdmin(admin.ModelAdmin):
|
class SupporterAdmin(admin.ModelAdmin):
|
||||||
list_display = ('display_name', 'display_until_date')
|
list_display = ('display_name', 'display_until_date')
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ strategies that defend FOSS (such as copyleft). <a href="/about" class="orange">
|
||||||
<section class="mh0 pa3 bg-light-blue ba b--gray">
|
<section class="mh0 pa3 bg-light-blue ba b--gray">
|
||||||
<video style="height: auto;" controls="" poster="/videos/sfc-introduction-video_poster.jpg">
|
<video style="height: auto;" controls="" poster="/videos/sfc-introduction-video_poster.jpg">
|
||||||
<source src="/videos/sfc-introduction_1080p.mp4" />
|
<source src="/videos/sfc-introduction_1080p.mp4" />
|
||||||
<track src="/docs/sfc-introduction-vtt-captions.txt" kind="subtitles" srclang="en" label="English" />
|
<track src="{% static 'docs/sfc-introduction-vtt-captions.txt' %}" kind="subtitles" srclang="en" label="English" />
|
||||||
<a href="/videos/sfc-introduction_1080p.mp4"><img src="/videos/sfc-introduction-video_poster.jpg" alt="Software Freedom Conservancy introduction video"></a><br/>
|
<a href="/videos/sfc-introduction_1080p.mp4"><img src="/videos/sfc-introduction-video_poster.jpg" alt="Software Freedom Conservancy introduction video"></a><br/>
|
||||||
<a href="https://youtu.be/yCCxMfW0LTM">(watch on Youtube)</a>
|
<a href="https://youtu.be/yCCxMfW0LTM">(watch on Youtube)</a>
|
||||||
</video>
|
</video>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .emails import make_candidate_email
|
from .emails import make_candidate_email
|
||||||
from .models import Candidate, Comment
|
from .models import Candidate, Comment, SourceOffer
|
||||||
|
|
||||||
|
|
||||||
class CommentInline(admin.TabularInline):
|
class CommentInline(admin.TabularInline):
|
||||||
|
@ -36,3 +36,10 @@ class CandidateAdmin(admin.ModelAdmin):
|
||||||
# Announce the new candidate
|
# Announce the new candidate
|
||||||
email = make_candidate_email(obj, request.user)
|
email = make_candidate_email(obj, request.user)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(SourceOffer)
|
||||||
|
class SourceOfferAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['time', 'vendor', 'device']
|
||||||
|
fields = ['time', 'vendor', 'device', 'photo']
|
||||||
|
readonly_fields = ['time']
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from .models import Comment
|
from .models import Comment, SourceOffer
|
||||||
|
|
||||||
|
|
||||||
class CommentForm(forms.ModelForm):
|
class CommentForm(forms.ModelForm):
|
||||||
|
@ -17,3 +17,14 @@ class CommentForm(forms.ModelForm):
|
||||||
|
|
||||||
class DownloadForm(forms.Form):
|
class DownloadForm(forms.Form):
|
||||||
agree = forms.BooleanField(label="I understand that the goal of this process is to determine compliance with FOSS licenses, and that in downloading the source code candidate and/or firmware image, I am assisting SFC as a volunteer to investigate that question. I, therefore, promise and represent that I will not copy, distribute, modify, or otherwise use this source code candidate and/or firmware image for any purpose other than to help SFC evaluate the source code candidate for compliance with the terms of FOSS licenses, including but not limited to any version of the GNU General Public License. Naturally, if I determine in good faith that portions of the source code candidate and/or firmware image are subject to a FOSS license and are compliant with it, I may copy, distribute, modify, or otherwise use those portions in accordance with the FOSS license, and I take full responsibility for that determination and subsequent use.")
|
agree = forms.BooleanField(label="I understand that the goal of this process is to determine compliance with FOSS licenses, and that in downloading the source code candidate and/or firmware image, I am assisting SFC as a volunteer to investigate that question. I, therefore, promise and represent that I will not copy, distribute, modify, or otherwise use this source code candidate and/or firmware image for any purpose other than to help SFC evaluate the source code candidate for compliance with the terms of FOSS licenses, including but not limited to any version of the GNU General Public License. Naturally, if I determine in good faith that portions of the source code candidate and/or firmware image are subject to a FOSS license and are compliant with it, I may copy, distribute, modify, or otherwise use those portions in accordance with the FOSS license, and I take full responsibility for that determination and subsequent use.")
|
||||||
|
|
||||||
|
|
||||||
|
class SourceOfferForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = SourceOffer
|
||||||
|
fields = ['vendor', 'device', 'photo']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['photo'].widget.attrs['capture'] = 'camera'
|
||||||
|
self.fields['photo'].widget.attrs['accept'] = 'image/*'
|
||||||
|
|
30
conservancy/usethesource/migrations/0009_sourceoffer.py
Normal file
30
conservancy/usethesource/migrations/0009_sourceoffer.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 4.2.11 on 2024-07-22 08:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('usethesource', '0008_comment_attribute_to'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SourceOffer',
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
'id',
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name='ID',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
('vendor', models.CharField(max_length=50, verbose_name='Vendor name')),
|
||||||
|
('device', models.CharField(max_length=50, verbose_name='Device name')),
|
||||||
|
('photo', models.ImageField(upload_to='usethesource/offers')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
18
conservancy/usethesource/migrations/0010_sourceoffer_time.py
Normal file
18
conservancy/usethesource/migrations/0010_sourceoffer_time.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.2.11 on 2024-07-29 09:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('usethesource', '0009_sourceoffer'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sourceoffer',
|
||||||
|
name='time',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -67,3 +67,13 @@ class Comment(models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['id']
|
ordering = ['id']
|
||||||
|
|
||||||
|
|
||||||
|
class SourceOffer(models.Model):
|
||||||
|
time = models.DateTimeField(auto_now_add=True, null=True)
|
||||||
|
vendor = models.CharField('Vendor name', max_length=50)
|
||||||
|
device = models.CharField('Device name', max_length=50)
|
||||||
|
photo = models.ImageField(upload_to='usethesource/offers')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.vendor} {self.device}'
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<p>One crucial way to get involved is to let us know about any source candidates you find! Many devices have an offer for source code (check the manual or device's user interface to find it) and we'd be very interested to know what they send you when you request it. Here are the steps to submit a new source candidate to list on this page:</p>
|
<p>One crucial way to get involved is to let us know about any source candidates you find! Many devices have an offer for source code (check the manual or device's user interface to find it) and we'd be very interested to know what they send you when you request it. Here are the steps to submit a new source candidate to list on this page:</p>
|
||||||
|
|
||||||
<ol class="pl4">
|
<ol class="pl4">
|
||||||
<li class="mb2">find a source candidate offered by a company - normally this is offered to you in the manual or user interface of your device, through a link or email address (the company's GitHub page is not canonical, unless they explicitly say so in this offer)</li>
|
<li class="mb2">find a source candidate offered by a company - normally this is offered to you in the manual or user interface of your device, through a link or email address (the company's GitHub page is not canonical, unless they explicitly say so in this offer). If you're curious what an offer is, check out the PDFs referenced in <a href="https://sfconservancy.org/blog/2022/dec/21/energyguide-software-repair-label/">our submission to the FTC</a>, and <a href="{% url 'usethesource:upload_offer' %}">submit a picture/image of a new offer</a> so we can test it for you if you like</li>
|
||||||
|
|
||||||
<li class="mb2"><a href="https://usl-upload.sfconservancy.org/s/4Ykmx7rSGMJ7s43">upload the source candidate</a> to us - write down the file name(s) you uploaded for the next step (can be multiple), and upload a firmware image if you have it and are ok with us publishing it</li>
|
<li class="mb2"><a href="https://usl-upload.sfconservancy.org/s/4Ykmx7rSGMJ7s43">upload the source candidate</a> to us - write down the file name(s) you uploaded for the next step (can be multiple), and upload a firmware image if you have it and are ok with us publishing it</li>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
{% extends "usethesource/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Upload an offer for source - Software Freedom Conservancy{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
<section class="mt4 mb3">
|
||||||
|
<h2 class="f2 lh-title ttu mt0">Upload an offer for source</h2>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<form id="form" hx-encoding="multipart/form-data" hx-post="{% url 'usethesource:upload_offer' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
<div class="mv2">
|
||||||
|
{{ form.vendor.errors }}
|
||||||
|
<label for="{{ form.vendor.id_for_label }}" class="db mb1">Vendor:</label>
|
||||||
|
{{ form.vendor }}
|
||||||
|
</div>
|
||||||
|
<div class="mv2">
|
||||||
|
{{ form.device.errors }}
|
||||||
|
<label for="{{ form.device.id_for_label }}" class="db mb1">Device:</label>
|
||||||
|
{{ form.device }}
|
||||||
|
</div>
|
||||||
|
<div class="mv2">
|
||||||
|
{{ form.photo.errors }}
|
||||||
|
<label for="{{ form.photo.id_for_label }}" class="db mb1">Photo:</label>
|
||||||
|
{{ form.photo }}
|
||||||
|
</div>
|
||||||
|
<progress id="progress" class="htmx-indicator" value="0" max="100"></progress>
|
||||||
|
<div class="mv1">
|
||||||
|
<button type="submit" class="white bg-green b db pv2 ph3 bn mb2">Send</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
form = document.querySelector('#form');
|
||||||
|
let progress = document.querySelector('#progress');
|
||||||
|
form.addEventListener('htmx:xhr:progress', function(evt) {
|
||||||
|
console.log('progress', evt.detail.loaded/evt.detail.total * 100);
|
||||||
|
progress.value = evt.detail.loaded/evt.detail.total * 100;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock content %}
|
|
@ -0,0 +1 @@
|
||||||
|
<p>Thanks! We've received your offer for source.</p>
|
|
@ -13,4 +13,5 @@ urlpatterns = [
|
||||||
path('delete-comment/<int:comment_id>/<show_add>/', views.delete_comment, name='delete_comment'),
|
path('delete-comment/<int:comment_id>/<show_add>/', views.delete_comment, name='delete_comment'),
|
||||||
path('add-button/<slug:slug>/', views.add_button, name='add_button'),
|
path('add-button/<slug:slug>/', views.add_button, name='add_button'),
|
||||||
path('ccirt-process/', views.ccirt_process, name='ccirt_process'),
|
path('ccirt-process/', views.ccirt_process, name='ccirt_process'),
|
||||||
|
path('offer/', views.upload_offer, name='upload_offer'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.core.exceptions import PermissionDenied
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
|
||||||
from .models import Candidate, Comment
|
from .models import Candidate, Comment
|
||||||
from .forms import CommentForm, DownloadForm
|
from .forms import CommentForm, DownloadForm, SourceOfferForm
|
||||||
from .emails import make_comment_email
|
from .emails import make_comment_email
|
||||||
|
|
||||||
|
|
||||||
|
@ -91,3 +91,21 @@ def add_button(request, slug):
|
||||||
|
|
||||||
def ccirt_process(request):
|
def ccirt_process(request):
|
||||||
return render(request, 'usethesource/ccirt_process.html', {})
|
return render(request, 'usethesource/ccirt_process.html', {})
|
||||||
|
|
||||||
|
|
||||||
|
def handle_uploaded_file(f):
|
||||||
|
with open("some/file/name.txt", "wb+") as destination:
|
||||||
|
for chunk in f.chunks():
|
||||||
|
destination.write(chunk)
|
||||||
|
|
||||||
|
def upload_offer(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = SourceOfferForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
return render(request, 'usethesource/upload_success_partial.html')
|
||||||
|
else:
|
||||||
|
return render(request, 'usethesource/upload_offer.html', {'form': form})
|
||||||
|
else:
|
||||||
|
form = SourceOfferForm()
|
||||||
|
return render(request, 'usethesource/upload_offer.html', {'form': form})
|
||||||
|
|
|
@ -6,4 +6,3 @@ from .models import EarthLocation
|
||||||
@admin.register(EarthLocation)
|
@admin.register(EarthLocation)
|
||||||
class EarthLocationAdmin(admin.ModelAdmin):
|
class EarthLocationAdmin(admin.ModelAdmin):
|
||||||
list_display = ("label", "html_map_link")
|
list_display = ("label", "html_map_link")
|
||||||
|
|
||||||
|
|
|
@ -26,4 +26,3 @@ class EarthLocation(models.Model):
|
||||||
def html_map_link(self): # for Admin, fixme: fix_ampersands
|
def html_map_link(self): # for Admin, fixme: fix_ampersands
|
||||||
return '<a href="%s">map link</a>' % self.default_map_link()
|
return '<a href="%s">map link</a>' % self.default_map_link()
|
||||||
html_map_link.allow_tags = True
|
html_map_link.allow_tags = True
|
||||||
|
|
||||||
|
|
18
pyproject.toml
Normal file
18
pyproject.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[tool.black]
|
||||||
|
skip-string-normalization = true
|
||||||
|
line-length = 90
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
|
force_sort_within_sections = true
|
||||||
|
line_length = 90
|
||||||
|
sections = "FUTURE,STDLIB,THIRDPARTY,LOCALFOLDER,FIRSTPARTY"
|
||||||
|
no_lines_before = "FIRSTPARTY"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
DJANGO_SETTINGS_MODULE = 'conservancy.settings.dev'
|
||||||
|
python_files = ['test*.py']
|
||||||
|
# pytest-django will default to running tests with DEBUG = False, regardless of
|
||||||
|
# the settings you provide it. This fails due to the
|
||||||
|
# `ManifestStaticFilesStorage` without explicitly running `collectstatic` first.
|
||||||
|
django_debug_mode = true
|
Loading…
Reference in a new issue