/fetch-nfts/", views.FetchNFTsView.as_view(), name="fetch-nfts"),
18 | ]
19 |
--------------------------------------------------------------------------------
/djsniper/sniper/views.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from celery.result import AsyncResult
4 |
5 | from django.http import HttpResponse
6 | from django.shortcuts import render, redirect
7 | from django.urls import reverse
8 | from django.views import generic
9 | from django.views.generic.detail import SingleObjectMixin
10 | from djsniper.sniper.forms import ConfirmForm, ProjectForm
11 | from djsniper.sniper.models import NFTAttribute, NFTProject
12 | from djsniper.sniper.tasks import fetch_nfts_task
13 |
14 |
15 | class ProjectListView(generic.ListView):
16 | template_name = "sniper/project_list.html"
17 |
18 | def get_queryset(self):
19 | return NFTProject.objects.all()
20 |
21 |
22 | class ProjectDetailView(generic.DetailView):
23 | template_name = "sniper/project_detail.html"
24 |
25 | def get_queryset(self):
26 | return NFTProject.objects.all()
27 |
28 | def get_context_data(self, **kwargs):
29 | context = super().get_context_data(**kwargs)
30 | nft_project = self.get_object()
31 | order = self.request.GET.get("order", None)
32 | nfts = nft_project.nfts.all()
33 | if order == "rank":
34 | nfts = nfts.order_by("rank")
35 | context.update({"nfts": nfts[0:12]})
36 | return context
37 |
38 |
39 | class ProjectCreateView(generic.CreateView):
40 | template_name = "sniper/project_create.html"
41 | form_class = ProjectForm
42 |
43 | def form_valid(self, form):
44 | instance = form.save()
45 | return redirect("sniper:project-detail", pk=instance.id)
46 |
47 | def get_queryset(self):
48 | return NFTProject.objects.all()
49 |
50 |
51 | class ProjectUpdateView(generic.UpdateView):
52 | template_name = "sniper/project_update.html"
53 | form_class = ProjectForm
54 |
55 | def get_queryset(self):
56 | return NFTProject.objects.all()
57 |
58 | def get_success_url(self):
59 | return reverse("sniper:project-detail", kwargs={"pk": self.get_object().id})
60 |
61 |
62 | class ProjectDeleteView(generic.DeleteView):
63 | template_name = "sniper/project_delete.html"
64 |
65 | def get_queryset(self):
66 | return NFTProject.objects.all()
67 |
68 | def get_success_url(self):
69 | return reverse("sniper:project-list")
70 |
71 |
72 | class ProjectClearView(SingleObjectMixin, generic.FormView):
73 | template_name = "sniper/project_clear.html"
74 | form_class = ConfirmForm
75 |
76 | def get(self, request, *args, **kwargs):
77 | self.object = self.get_object()
78 | context = self.get_context_data(object=self.object)
79 | return self.render_to_response(context)
80 |
81 | def get_queryset(self):
82 | return NFTProject.objects.all()
83 |
84 | def form_valid(self, form):
85 | nft_project = self.get_object()
86 | nft_project.nfts.all().delete()
87 | NFTAttribute.objects.filter(project=nft_project).delete()
88 | return super().form_valid(form)
89 |
90 | def get_success_url(self):
91 | return reverse("sniper:project-detail", kwargs={"pk": self.kwargs["pk"]})
92 |
93 |
94 | def nft_list(request):
95 | project = NFTProject.objects.get(name="BAYC")
96 | nfts = project.nfts.all().order_by("-rarity_score")[0:12]
97 | return render(request, "nfts.html", {"nfts": nfts})
98 |
99 |
100 | class FetchNFTsView(generic.FormView):
101 | template_name = "sniper/fetch_nfts.html"
102 | form_class = ConfirmForm
103 |
104 | def form_valid(self, form):
105 | result = fetch_nfts_task.apply_async((self.kwargs["pk"],), countdown=1)
106 | return render(self.request, self.template_name, {"task_id": result.task_id})
107 |
108 |
109 | def get_progress(request, task_id):
110 | result = AsyncResult(task_id)
111 | response_data = {
112 | "state": result.state,
113 | "details": result.info,
114 | }
115 | return HttpResponse(json.dumps(response_data), content_type="application/json")
116 |
--------------------------------------------------------------------------------
/djsniper/static/css/project.css:
--------------------------------------------------------------------------------
1 | /* These styles are generated from project.scss. */
2 |
3 | .alert-debug {
4 | color: black;
5 | background-color: white;
6 | border-color: #d6e9c6;
7 | }
8 |
9 | .alert-error {
10 | color: #b94a48;
11 | background-color: #f2dede;
12 | border-color: #eed3d7;
13 | }
14 |
--------------------------------------------------------------------------------
/djsniper/static/fonts/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/static/fonts/.gitkeep
--------------------------------------------------------------------------------
/djsniper/static/images/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/static/images/favicons/favicon.ico
--------------------------------------------------------------------------------
/djsniper/static/js/celery_progress.js:
--------------------------------------------------------------------------------
1 | class CeleryProgressBar {
2 |
3 | constructor(progressUrl, options) {
4 | this.progressUrl = progressUrl;
5 | options = options || {};
6 | let progressBarId = options.progressBarId || 'progress-bar';
7 | let progressBarMessage = options.progressBarMessageId || 'progress-bar-message';
8 | this.progressBarElement = options.progressBarElement || document.getElementById(progressBarId);
9 | this.progressBarMessageElement = options.progressBarMessageElement || document.getElementById(progressBarMessage);
10 | this.onProgress = options.onProgress || this.onProgressDefault;
11 | this.onSuccess = options.onSuccess || this.onSuccessDefault;
12 | this.onError = options.onError || this.onErrorDefault;
13 | this.onTaskError = options.onTaskError || this.onTaskErrorDefault;
14 | this.onDataError = options.onDataError || this.onError;
15 | this.onRetry = options.onRetry || this.onRetryDefault;
16 | this.onIgnored = options.onIgnored || this.onIgnoredDefault;
17 | let resultElementId = options.resultElementId || 'celery-result';
18 | this.resultElement = options.resultElement || document.getElementById(resultElementId);
19 | this.onResult = options.onResult || this.onResultDefault;
20 | // HTTP options
21 | this.onNetworkError = options.onNetworkError || this.onError;
22 | this.onHttpError = options.onHttpError || this.onError;
23 | this.pollInterval = options.pollInterval || 500;
24 | // Other options
25 | let barColorsDefault = {
26 | success: '#76ce60',
27 | error: '#dc4f63',
28 | progress: '#68a9ef',
29 | ignored: '#7a7a7a'
30 | }
31 | this.barColors = Object.assign({}, barColorsDefault, options.barColors);
32 |
33 | let defaultMessages = {
34 | waiting: 'Waiting for task to start...',
35 | started: 'Task started...',
36 | }
37 | this.messages = Object.assign({}, defaultMessages, options.defaultMessages);
38 | }
39 |
40 | onSuccessDefault(progressBarElement, progressBarMessageElement, result) {
41 | result = this.getMessageDetails(result);
42 | if (progressBarElement) {
43 | progressBarElement.style.backgroundColor = this.barColors.success;
44 | }
45 | if (progressBarMessageElement) {
46 | progressBarMessageElement.textContent = "Success! " + result;
47 | }
48 | }
49 |
50 | onResultDefault(resultElement, result) {
51 | if (resultElement) {
52 | resultElement.textContent = result;
53 | }
54 | }
55 |
56 | /**
57 | * Default handler for all errors.
58 | * @param data - A Response object for HTTP errors, undefined for other errors
59 | */
60 | onErrorDefault(progressBarElement, progressBarMessageElement, excMessage, data) {
61 | progressBarElement.style.backgroundColor = this.barColors.error;
62 | excMessage = excMessage || '';
63 | progressBarMessageElement.textContent = "Uh-Oh, something went wrong! " + excMessage;
64 | }
65 |
66 | onTaskErrorDefault(progressBarElement, progressBarMessageElement, excMessage) {
67 | let message = this.getMessageDetails(excMessage);
68 | this.onError(progressBarElement, progressBarMessageElement, message);
69 | }
70 |
71 | onRetryDefault(progressBarElement, progressBarMessageElement, excMessage, retryWhen) {
72 | retryWhen = new Date(retryWhen);
73 | let message = 'Retrying in ' + Math.round((retryWhen.getTime() - Date.now())/1000) + 's: ' + excMessage;
74 | this.onError(progressBarElement, progressBarMessageElement, message);
75 | }
76 |
77 | onIgnoredDefault(progressBarElement, progressBarMessageElement, result) {
78 | progressBarElement.style.backgroundColor = this.barColors.ignored;
79 | progressBarMessageElement.textContent = result || 'Task result ignored!'
80 | }
81 |
82 | onProgressDefault(progressBarElement, progressBarMessageElement, progress) {
83 | progressBarElement.style.backgroundColor = this.barColors.progress;
84 | progressBarElement.style.width = progress.percent + "%";
85 | var description = progress.description || "";
86 | if (progress.current == 0) {
87 | if (progress.pending === true) {
88 | progressBarMessageElement.textContent = this.messages.waiting;
89 | } else {
90 | progressBarMessageElement.textContent = this.messages.started;
91 | }
92 | } else {
93 | progressBarMessageElement.textContent = progress.current + ' of ' + progress.total + ' processed. ' + description;
94 | }
95 | }
96 |
97 | getMessageDetails(result) {
98 | if (this.resultElement) {
99 | return ''
100 | } else {
101 | return result || '';
102 | }
103 | }
104 |
105 | /**
106 | * Process update message data.
107 | * @return true if the task is complete, false if it's not, undefined if `data` is invalid
108 | */
109 | onData(data) {
110 | let done = false;
111 | if (data.progress) {
112 | this.onProgress(this.progressBarElement, this.progressBarMessageElement, data.progress);
113 | }
114 | if (data.complete === true) {
115 | done = true;
116 | if (data.success === true) {
117 | this.onSuccess(this.progressBarElement, this.progressBarMessageElement, data.result);
118 | } else if (data.success === false) {
119 | if (data.state === 'RETRY') {
120 | this.onRetry(this.progressBarElement, this.progressBarMessageElement, data.result.message, data.result.when);
121 | done = false;
122 | delete data.result;
123 | } else {
124 | this.onTaskError(this.progressBarElement, this.progressBarMessageElement, data.result);
125 | }
126 | } else {
127 | if (data.state === 'IGNORED') {
128 | this.onIgnored(this.progressBarElement, this.progressBarMessageElement, data.result);
129 | delete data.result;
130 | } else {
131 | done = undefined;
132 | this.onDataError(this.progressBarElement, this.progressBarMessageElement, "Data Error");
133 | }
134 | }
135 | if (data.hasOwnProperty('result')) {
136 | this.onResult(this.resultElement, data.result);
137 | }
138 | } else if (data.complete === undefined) {
139 | done = undefined;
140 | this.onDataError(this.progressBarElement, this.progressBarMessageElement, "Data Error");
141 | }
142 | return done;
143 | }
144 |
145 | async connect() {
146 | let response;
147 | try {
148 | response = await fetch(this.progressUrl);
149 | } catch (networkError) {
150 | this.onNetworkError(this.progressBarElement, this.progressBarMessageElement, "Network Error");
151 | throw networkError;
152 | }
153 |
154 | if (response.status === 200) {
155 | let data;
156 | try {
157 | data = await response.json();
158 | } catch (parsingError) {
159 | this.onDataError(this.progressBarElement, this.progressBarMessageElement, "Parsing Error")
160 | throw parsingError;
161 | }
162 |
163 | const complete = this.onData(data);
164 |
165 | if (complete === false) {
166 | setTimeout(this.connect.bind(this), this.pollInterval);
167 | }
168 | } else {
169 | this.onHttpError(this.progressBarElement, this.progressBarMessageElement, "HTTP Code " + response.status, response);
170 | }
171 | }
172 |
173 | static initProgressBar(progressUrl, options) {
174 | const bar = new this(progressUrl, options);
175 | bar.connect();
176 | }
177 | }
--------------------------------------------------------------------------------
/djsniper/static/js/project.js:
--------------------------------------------------------------------------------
1 | /* Project specific Javascript goes here. */
2 |
--------------------------------------------------------------------------------
/djsniper/static/sass/custom_bootstrap_vars.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/static/sass/custom_bootstrap_vars.scss
--------------------------------------------------------------------------------
/djsniper/static/sass/project.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | // project specific CSS goes here
6 |
7 | ////////////////////////////////
8 | //Variables//
9 | ////////////////////////////////
10 |
11 | // Alert colors
12 |
13 | $white: #fff;
14 | $mint-green: #d6e9c6;
15 | $black: #000;
16 | $pink: #f2dede;
17 | $dark-pink: #eed3d7;
18 | $red: #b94a48;
19 |
20 | ////////////////////////////////
21 | //Alerts//
22 | ////////////////////////////////
23 |
24 | // bootstrap alert CSS, translated to the django-standard levels of
25 | // debug, info, success, warning, error
26 |
27 | .alert-debug {
28 | background-color: $white;
29 | border-color: $mint-green;
30 | color: $black;
31 | }
32 |
33 | .alert-error {
34 | background-color: $pink;
35 | border-color: $dark-pink;
36 | color: $red;
37 | }
38 |
--------------------------------------------------------------------------------
/djsniper/templates/403.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Forbidden (403){% endblock %}
4 |
5 | {% block content %}
6 | Forbidden (403)
7 |
8 | {% if exception %}{{ exception }}{% else %}You're not allowed to access this page.{% endif %}
9 | {% endblock content %}
10 |
--------------------------------------------------------------------------------
/djsniper/templates/404.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Page not found{% endblock %}
4 |
5 | {% block content %}
6 | Page not found
7 |
8 | {% if exception %}{{ exception }}{% else %}This is not the page you were looking for.{% endif %}
9 | {% endblock content %}
10 |
--------------------------------------------------------------------------------
/djsniper/templates/500.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Server Error{% endblock %}
4 |
5 | {% block content %}
6 | Ooops!!! 500
7 |
8 | Looks like something went wrong!
9 |
10 | We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.
11 | {% endblock content %}
12 |
--------------------------------------------------------------------------------
/djsniper/templates/account/account_inactive.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}{% translate "Account Inactive" %}{% endblock %}
6 |
7 | {% block inner %}
8 | {% translate "Account Inactive" %}
9 |
10 | {% translate "This account is inactive." %}
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/djsniper/templates/account/base.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}{% block head_title %}{% endblock head_title %}{% endblock title %}
3 |
4 | {% block content %}
5 |
6 |
7 | {% block inner %}{% endblock %}
8 |
9 |
10 | {% endblock %}
11 |
--------------------------------------------------------------------------------
/djsniper/templates/account/email.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "account/base.html" %}
3 |
4 | {% load i18n %}
5 | {% load crispy_forms_tags %}
6 |
7 | {% block head_title %}{% translate "Account" %}{% endblock %}
8 |
9 | {% block inner %}
10 | {% translate "E-mail Addresses" %}
11 |
12 | {% if user.emailaddress_set.all %}
13 | {% translate 'The following e-mail addresses are associated with your account:' %}
14 |
15 |
44 |
45 | {% else %}
46 | {% translate 'Warning:'%} {% translate "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}
47 |
48 | {% endif %}
49 |
50 |
51 | {% translate "Add E-mail Address" %}
52 |
53 |
58 |
59 | {% endblock %}
60 |
61 |
62 | {% block inline_javascript %}
63 | {{ block.super }}
64 |
78 | {% endblock %}
79 |
--------------------------------------------------------------------------------
/djsniper/templates/account/email_confirm.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load account %}
5 |
6 | {% block head_title %}{% translate "Confirm E-mail Address" %}{% endblock %}
7 |
8 |
9 | {% block inner %}
10 | {% translate "Confirm E-mail Address" %}
11 |
12 | {% if confirmation %}
13 |
14 | {% user_display confirmation.email_address.user as user_display %}
15 |
16 | {% blocktranslate with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktranslate %}
17 |
18 |
22 |
23 | {% else %}
24 |
25 | {% url 'account_email' as email_url %}
26 |
27 | {% blocktranslate %}This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request .{% endblocktranslate %}
28 |
29 | {% endif %}
30 |
31 | {% endblock %}
32 |
--------------------------------------------------------------------------------
/djsniper/templates/account/login.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load account socialaccount %}
5 | {% load crispy_forms_tags %}
6 |
7 | {% block head_title %}{% translate "Sign In" %}{% endblock %}
8 |
9 | {% block inner %}
10 |
11 | {% translate "Sign In" %}
12 |
13 | {% get_providers as socialaccount_providers %}
14 |
15 | {% if socialaccount_providers %}
16 | {% blocktranslate with site.name as site_name %}Please sign in with one
17 | of your existing third party accounts. Or, sign up
18 | for a {{ site_name }} account and sign in below:{% endblocktranslate %}
19 |
20 |
21 |
22 |
23 | {% include "socialaccount/snippets/provider_list.html" with process="login" %}
24 |
25 |
26 |
{% translate 'or' %}
27 |
28 |
29 |
30 | {% include "socialaccount/snippets/login_extra.html" %}
31 |
32 | {% else %}
33 | {% blocktranslate %}If you have not created an account yet, then please
34 | sign up first.{% endblocktranslate %}
35 | {% endif %}
36 |
37 |
46 |
47 | {% endblock %}
48 |
--------------------------------------------------------------------------------
/djsniper/templates/account/logout.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}{% translate "Sign Out" %}{% endblock %}
6 |
7 | {% block inner %}
8 | {% translate "Sign Out" %}
9 |
10 | {% translate 'Are you sure you want to sign out?' %}
11 |
12 |
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/djsniper/templates/account/password_change.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load crispy_forms_tags %}
5 |
6 | {% block head_title %}{% translate "Change Password" %}{% endblock %}
7 |
8 | {% block inner %}
9 | {% translate "Change Password" %}
10 |
11 |
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/djsniper/templates/account/password_reset.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load account %}
5 | {% load crispy_forms_tags %}
6 |
7 | {% block head_title %}{% translate "Password Reset" %}{% endblock %}
8 |
9 | {% block inner %}
10 |
11 | {% translate "Password Reset" %}
12 | {% if user.is_authenticated %}
13 | {% include "account/snippets/already_logged_in.html" %}
14 | {% endif %}
15 |
16 | {% translate "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}
17 |
18 |
23 |
24 | {% blocktranslate %}Please contact us if you have any trouble resetting your password.{% endblocktranslate %}
25 | {% endblock %}
26 |
--------------------------------------------------------------------------------
/djsniper/templates/account/password_reset_done.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load account %}
5 |
6 | {% block head_title %}{% translate "Password Reset" %}{% endblock %}
7 |
8 | {% block inner %}
9 | {% translate "Password Reset" %}
10 |
11 | {% if user.is_authenticated %}
12 | {% include "account/snippets/already_logged_in.html" %}
13 | {% endif %}
14 |
15 | {% blocktranslate %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktranslate %}
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/djsniper/templates/account/password_reset_from_key.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load crispy_forms_tags %}
5 | {% block head_title %}{% translate "Change Password" %}{% endblock %}
6 |
7 | {% block inner %}
8 | {% if token_fail %}{% translate "Bad Token" %}{% else %}{% translate "Change Password" %}{% endif %}
9 |
10 | {% if token_fail %}
11 | {% url 'account_reset_password' as passwd_reset_url %}
12 | {% blocktranslate %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset .{% endblocktranslate %}
13 | {% else %}
14 | {% if form %}
15 |
20 | {% else %}
21 | {% translate 'Your password is now changed.' %}
22 | {% endif %}
23 | {% endif %}
24 | {% endblock %}
25 |
--------------------------------------------------------------------------------
/djsniper/templates/account/password_reset_from_key_done.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% block head_title %}{% translate "Change Password" %}{% endblock %}
5 |
6 | {% block inner %}
7 | {% translate "Change Password" %}
8 | {% translate 'Your password is now changed.' %}
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/djsniper/templates/account/password_set.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load crispy_forms_tags %}
5 |
6 | {% block head_title %}{% translate "Set Password" %}{% endblock %}
7 |
8 | {% block inner %}
9 | {% translate "Set Password" %}
10 |
11 |
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/djsniper/templates/account/signup.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load crispy_forms_tags %}
5 |
6 | {% block head_title %}{% translate "Signup" %}{% endblock %}
7 |
8 | {% block inner %}
9 | {% translate "Sign Up" %}
10 |
11 | {% blocktranslate %}Already have an account? Then please sign in .{% endblocktranslate %}
12 |
13 |
21 |
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/djsniper/templates/account/signup_closed.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}{% translate "Sign Up Closed" %}{% endblock %}
6 |
7 | {% block inner %}
8 | {% translate "Sign Up Closed" %}
9 |
10 | {% translate "We are sorry, but the sign up is currently closed." %}
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/djsniper/templates/account/verification_sent.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}{% translate "Verify Your E-mail Address" %}{% endblock %}
6 |
7 | {% block inner %}
8 | {% translate "Verify Your E-mail Address" %}
9 |
10 | {% blocktranslate %}We have sent an e-mail to you for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktranslate %}
11 |
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/djsniper/templates/account/verified_email_required.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}{% translate "Verify Your E-mail Address" %}{% endblock %}
6 |
7 | {% block inner %}
8 | {% translate "Verify Your E-mail Address" %}
9 |
10 | {% url 'account_email' as email_url %}
11 |
12 | {% blocktranslate %}This part of the site requires us to verify that
13 | you are who you claim to be. For this purpose, we require that you
14 | verify ownership of your e-mail address. {% endblocktranslate %}
15 |
16 | {% blocktranslate %}We have sent an e-mail to you for
17 | verification. Please click on the link inside this e-mail. Please
18 | contact us if you do not receive it within a few minutes.{% endblocktranslate %}
19 |
20 | {% blocktranslate %}Note: you can still change your e-mail address .{% endblocktranslate %}
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------
/djsniper/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load static i18n %}
2 |
3 |
4 |
5 |
6 |
7 |
8 | {% block title %}Dj NFT Sniper{% endblock title %}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {% block css %}
17 |
18 |
19 |
20 | {% endblock %}
21 |
23 | {# Placed at the top of the document so pages load faster with defer #}
24 | {% block javascript %}
25 |
26 |
27 | {% endblock javascript %}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
62 |
63 |
64 |
65 | {% if messages %}
66 | {% for message in messages %}
67 |
68 | {{ message }}
69 |
70 |
71 | {% endfor %}
72 | {% endif %}
73 |
74 |
75 | {% block content %}
76 |
Use this document as a way to quick start any new project.
77 | {% endblock content %}
78 |
79 |
80 |
81 |
82 |
83 | {% block modal %}{% endblock modal %}
84 |
85 | {% block inline_javascript %}
86 | {% comment %}
87 | Script tags with only code, no src (defer by default). To run
88 | with a "defer" so that you run run inline code:
89 |
92 | {% endcomment %}
93 | {% endblock inline_javascript %}
94 |
95 |
96 |
--------------------------------------------------------------------------------
/djsniper/templates/nfts.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 | Bored Ape Yacht Club
7 |
8 |
9 | {% for nft in nfts %}
10 |
11 |
12 |
13 |
#{{ nft.nft_id }}
14 |
Score: {{ nft.rarity_score|floatformat:2 }}
15 |
16 |
17 | {% endfor %}
18 |
19 |
20 | {% endblock content %}
--------------------------------------------------------------------------------
/djsniper/templates/pages/about.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
--------------------------------------------------------------------------------
/djsniper/templates/sniper/fetch_nfts.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block content %}
3 | {% load static %}
4 |
5 |
6 |
Fetch NFTs
7 |
Confirming this form will start the celery tasks
8 |
9 | {% if task_id %}
10 |
11 |
16 |
Waiting for progress to start...
17 |
18 | {% else %}
19 |
20 |
28 |
29 | {% endif %}
30 |
31 |
32 |
33 | {% endblock content %}
34 |
35 | {% block inline_javascript %}
36 |
37 |
44 |
45 | {% endblock inline_javascript %}
--------------------------------------------------------------------------------
/djsniper/templates/sniper/project_clear.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load tailwind_filters %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 |
9 | Clear Project Data
10 |
11 |
12 |
13 |
18 |
19 |
24 |
25 |
26 |
27 | {% endblock content %}
--------------------------------------------------------------------------------
/djsniper/templates/sniper/project_create.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load tailwind_filters %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 | Create Project
9 |
10 |
11 |
17 |
18 |
19 | {% endblock content %}
--------------------------------------------------------------------------------
/djsniper/templates/sniper/project_delete.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load tailwind_filters %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 |
9 | Delete Project
10 |
11 |
12 |
13 |
18 |
19 |
24 |
25 |
26 |
27 | {% endblock content %}
--------------------------------------------------------------------------------
/djsniper/templates/sniper/project_detail.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
9 | {{ object.name }}
10 |
11 |
12 |
18 |
19 |
20 |
21 |
{{ object.number_of_nfts }} NFTs
22 |
23 | {% for nft in nfts %}
24 |
25 |
26 |
27 |
#{{ nft.nft_id }}
28 |
Score: {{ nft.rarity_score|floatformat:2 }}
29 |
30 |
31 | {% endfor %}
32 |
33 |
39 |
40 |
41 |
55 |
56 |
57 | {% endblock content %}
--------------------------------------------------------------------------------
/djsniper/templates/sniper/project_list.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 | Projects
7 |
8 |
9 | {% for project in object_list %}
10 |
11 |
18 |
19 | {% endfor %}
20 |
21 | {% endblock content %}
--------------------------------------------------------------------------------
/djsniper/templates/sniper/project_update.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load tailwind_filters %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 | Update {{ object.name }}
9 |
10 |
11 |
16 |
17 |
23 |
24 |
25 | {% endblock content %}
--------------------------------------------------------------------------------
/djsniper/templates/users/user_detail.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load static %}
3 |
4 | {% block title %}User: {{ object.username }}{% endblock %}
5 |
6 | {% block content %}
7 |
8 |
9 |
10 |
11 |
12 |
{{ object.username }}
13 | {% if object.name %}
14 |
{{ object.name }}
15 | {% endif %}
16 |
17 |
18 |
19 | {% if object == request.user %}
20 |
21 |
30 |
31 | {% endif %}
32 |
33 |
34 | {% endblock content %}
35 |
--------------------------------------------------------------------------------
/djsniper/templates/users/user_form.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load crispy_forms_tags %}
3 |
4 | {% block title %}{{ user.username }}{% endblock %}
5 |
6 | {% block content %}
7 | {{ user.username }}
8 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/djsniper/users/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/users/__init__.py
--------------------------------------------------------------------------------
/djsniper/users/adapters.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from allauth.account.adapter import DefaultAccountAdapter
4 | from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
5 | from django.conf import settings
6 | from django.http import HttpRequest
7 |
8 |
9 | class AccountAdapter(DefaultAccountAdapter):
10 | def is_open_for_signup(self, request: HttpRequest):
11 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
12 |
13 |
14 | class SocialAccountAdapter(DefaultSocialAccountAdapter):
15 | def is_open_for_signup(self, request: HttpRequest, sociallogin: Any):
16 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
17 |
--------------------------------------------------------------------------------
/djsniper/users/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.contrib.auth import admin as auth_admin
3 | from django.contrib.auth import get_user_model
4 | from django.utils.translation import gettext_lazy as _
5 |
6 | from djsniper.users.forms import UserChangeForm, UserCreationForm
7 |
8 | User = get_user_model()
9 |
10 |
11 | @admin.register(User)
12 | class UserAdmin(auth_admin.UserAdmin):
13 |
14 | form = UserChangeForm
15 | add_form = UserCreationForm
16 | fieldsets = (
17 | (None, {"fields": ("username", "password")}),
18 | (_("Personal info"), {"fields": ("name", "email")}),
19 | (
20 | _("Permissions"),
21 | {
22 | "fields": (
23 | "is_active",
24 | "is_staff",
25 | "is_superuser",
26 | "groups",
27 | "user_permissions",
28 | ),
29 | },
30 | ),
31 | (_("Important dates"), {"fields": ("last_login", "date_joined")}),
32 | )
33 | list_display = ["username", "name", "is_superuser"]
34 | search_fields = ["name"]
35 |
--------------------------------------------------------------------------------
/djsniper/users/api/serializers.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 | from rest_framework import serializers
3 |
4 | User = get_user_model()
5 |
6 |
7 | class UserSerializer(serializers.ModelSerializer):
8 | class Meta:
9 | model = User
10 | fields = ["username", "name", "url"]
11 |
12 | extra_kwargs = {
13 | "url": {"view_name": "api:user-detail", "lookup_field": "username"}
14 | }
15 |
--------------------------------------------------------------------------------
/djsniper/users/api/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 | from rest_framework import status
3 | from rest_framework.decorators import action
4 | from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin
5 | from rest_framework.response import Response
6 | from rest_framework.viewsets import GenericViewSet
7 |
8 | from .serializers import UserSerializer
9 |
10 | User = get_user_model()
11 |
12 |
13 | class UserViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, GenericViewSet):
14 | serializer_class = UserSerializer
15 | queryset = User.objects.all()
16 | lookup_field = "username"
17 |
18 | def get_queryset(self, *args, **kwargs):
19 | assert isinstance(self.request.user.id, int)
20 | return self.queryset.filter(id=self.request.user.id)
21 |
22 | @action(detail=False)
23 | def me(self, request):
24 | serializer = UserSerializer(request.user, context={"request": request})
25 | return Response(status=status.HTTP_200_OK, data=serializer.data)
26 |
--------------------------------------------------------------------------------
/djsniper/users/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.utils.translation import gettext_lazy as _
3 |
4 |
5 | class UsersConfig(AppConfig):
6 | name = "djsniper.users"
7 | verbose_name = _("Users")
8 |
9 | def ready(self):
10 | try:
11 | import djsniper.users.signals # noqa F401
12 | except ImportError:
13 | pass
14 |
--------------------------------------------------------------------------------
/djsniper/users/forms.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import forms as admin_forms
2 | from django.contrib.auth import get_user_model
3 | from django.utils.translation import gettext_lazy as _
4 |
5 | User = get_user_model()
6 |
7 |
8 | class UserChangeForm(admin_forms.UserChangeForm):
9 | class Meta(admin_forms.UserChangeForm.Meta):
10 | model = User
11 |
12 |
13 | class UserCreationForm(admin_forms.UserCreationForm):
14 | class Meta(admin_forms.UserCreationForm.Meta):
15 | model = User
16 |
17 | error_messages = {
18 | "username": {"unique": _("This username has already been taken.")}
19 | }
20 |
--------------------------------------------------------------------------------
/djsniper/users/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.9 on 2021-11-20 11:23
2 | import django.contrib.auth.models
3 | import django.contrib.auth.validators
4 | from django.db import migrations, models
5 | import django.utils.timezone
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ("auth", "0012_alter_user_first_name_max_length"),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name="User",
19 | fields=[
20 | (
21 | "id",
22 | models.BigAutoField(
23 | auto_created=True,
24 | primary_key=True,
25 | serialize=False,
26 | verbose_name="ID",
27 | ),
28 | ),
29 | ("password", models.CharField(max_length=128, verbose_name="password")),
30 | (
31 | "last_login",
32 | models.DateTimeField(
33 | blank=True, null=True, verbose_name="last login"
34 | ),
35 | ),
36 | (
37 | "is_superuser",
38 | models.BooleanField(
39 | default=False,
40 | help_text="Designates that this user has all permissions without explicitly assigning them.",
41 | verbose_name="superuser status",
42 | ),
43 | ),
44 | (
45 | "username",
46 | models.CharField(
47 | error_messages={
48 | "unique": "A user with that username already exists."
49 | },
50 | help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
51 | max_length=150,
52 | unique=True,
53 | validators=[
54 | django.contrib.auth.validators.UnicodeUsernameValidator()
55 | ],
56 | verbose_name="username",
57 | ),
58 | ),
59 | (
60 | "email",
61 | models.EmailField(
62 | blank=True, max_length=254, verbose_name="email address"
63 | ),
64 | ),
65 | (
66 | "is_staff",
67 | models.BooleanField(
68 | default=False,
69 | help_text="Designates whether the user can log into this admin site.",
70 | verbose_name="staff status",
71 | ),
72 | ),
73 | (
74 | "is_active",
75 | models.BooleanField(
76 | default=True,
77 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
78 | verbose_name="active",
79 | ),
80 | ),
81 | (
82 | "date_joined",
83 | models.DateTimeField(
84 | default=django.utils.timezone.now, verbose_name="date joined"
85 | ),
86 | ),
87 | (
88 | "name",
89 | models.CharField(
90 | blank=True, max_length=255, verbose_name="Name of User"
91 | ),
92 | ),
93 | (
94 | "groups",
95 | models.ManyToManyField(
96 | blank=True,
97 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
98 | related_name="user_set",
99 | related_query_name="user",
100 | to="auth.Group",
101 | verbose_name="groups",
102 | ),
103 | ),
104 | (
105 | "user_permissions",
106 | models.ManyToManyField(
107 | blank=True,
108 | help_text="Specific permissions for this user.",
109 | related_name="user_set",
110 | related_query_name="user",
111 | to="auth.Permission",
112 | verbose_name="user permissions",
113 | ),
114 | ),
115 | ],
116 | options={
117 | "verbose_name": "user",
118 | "verbose_name_plural": "users",
119 | "abstract": False,
120 | },
121 | managers=[
122 | ("objects", django.contrib.auth.models.UserManager()),
123 | ],
124 | ),
125 | ]
126 |
--------------------------------------------------------------------------------
/djsniper/users/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/users/migrations/__init__.py
--------------------------------------------------------------------------------
/djsniper/users/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import AbstractUser
2 | from django.db.models import CharField
3 | from django.urls import reverse
4 | from django.utils.translation import gettext_lazy as _
5 |
6 |
7 | class User(AbstractUser):
8 | """Default user for djsniper."""
9 |
10 | #: First and last name do not cover name patterns around the globe
11 | name = CharField(_("Name of User"), blank=True, max_length=255)
12 | first_name = None # type: ignore
13 | last_name = None # type: ignore
14 |
15 | def get_absolute_url(self):
16 | """Get url for user's detail view.
17 |
18 | Returns:
19 | str: URL for user detail.
20 |
21 | """
22 | return reverse("users:detail", kwargs={"username": self.username})
23 |
--------------------------------------------------------------------------------
/djsniper/users/tasks.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 |
3 | from config import celery_app
4 |
5 | User = get_user_model()
6 |
7 |
8 | @celery_app.task()
9 | def get_users_count():
10 | """A pointless Celery task to demonstrate usage."""
11 | return User.objects.count()
12 |
--------------------------------------------------------------------------------
/djsniper/users/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/users/tests/__init__.py
--------------------------------------------------------------------------------
/djsniper/users/tests/factories.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Sequence
2 |
3 | from django.contrib.auth import get_user_model
4 | from factory import Faker, post_generation
5 | from factory.django import DjangoModelFactory
6 |
7 |
8 | class UserFactory(DjangoModelFactory):
9 |
10 | username = Faker("user_name")
11 | email = Faker("email")
12 | name = Faker("name")
13 |
14 | @post_generation
15 | def password(self, create: bool, extracted: Sequence[Any], **kwargs):
16 | password = (
17 | extracted
18 | if extracted
19 | else Faker(
20 | "password",
21 | length=42,
22 | special_chars=True,
23 | digits=True,
24 | upper_case=True,
25 | lower_case=True,
26 | ).evaluate(None, None, extra={"locale": None})
27 | )
28 | self.set_password(password)
29 |
30 | class Meta:
31 | model = get_user_model()
32 | django_get_or_create = ["username"]
33 |
--------------------------------------------------------------------------------
/djsniper/users/tests/test_admin.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.urls import reverse
3 |
4 | from djsniper.users.models import User
5 |
6 | pytestmark = pytest.mark.django_db
7 |
8 |
9 | class TestUserAdmin:
10 | def test_changelist(self, admin_client):
11 | url = reverse("admin:users_user_changelist")
12 | response = admin_client.get(url)
13 | assert response.status_code == 200
14 |
15 | def test_search(self, admin_client):
16 | url = reverse("admin:users_user_changelist")
17 | response = admin_client.get(url, data={"q": "test"})
18 | assert response.status_code == 200
19 |
20 | def test_add(self, admin_client):
21 | url = reverse("admin:users_user_add")
22 | response = admin_client.get(url)
23 | assert response.status_code == 200
24 |
25 | response = admin_client.post(
26 | url,
27 | data={
28 | "username": "test",
29 | "password1": "My_R@ndom-P@ssw0rd",
30 | "password2": "My_R@ndom-P@ssw0rd",
31 | },
32 | )
33 | assert response.status_code == 302
34 | assert User.objects.filter(username="test").exists()
35 |
36 | def test_view_user(self, admin_client):
37 | user = User.objects.get(username="admin")
38 | url = reverse("admin:users_user_change", kwargs={"object_id": user.pk})
39 | response = admin_client.get(url)
40 | assert response.status_code == 200
41 |
--------------------------------------------------------------------------------
/djsniper/users/tests/test_drf_urls.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.urls import resolve, reverse
3 |
4 | from djsniper.users.models import User
5 |
6 | pytestmark = pytest.mark.django_db
7 |
8 |
9 | def test_user_detail(user: User):
10 | assert (
11 | reverse("api:user-detail", kwargs={"username": user.username})
12 | == f"/api/users/{user.username}/"
13 | )
14 | assert resolve(f"/api/users/{user.username}/").view_name == "api:user-detail"
15 |
16 |
17 | def test_user_list():
18 | assert reverse("api:user-list") == "/api/users/"
19 | assert resolve("/api/users/").view_name == "api:user-list"
20 |
21 |
22 | def test_user_me():
23 | assert reverse("api:user-me") == "/api/users/me/"
24 | assert resolve("/api/users/me/").view_name == "api:user-me"
25 |
--------------------------------------------------------------------------------
/djsniper/users/tests/test_drf_views.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.test import RequestFactory
3 |
4 | from djsniper.users.api.views import UserViewSet
5 | from djsniper.users.models import User
6 |
7 | pytestmark = pytest.mark.django_db
8 |
9 |
10 | class TestUserViewSet:
11 | def test_get_queryset(self, user: User, rf: RequestFactory):
12 | view = UserViewSet()
13 | request = rf.get("/fake-url/")
14 | request.user = user
15 |
16 | view.request = request
17 |
18 | assert user in view.get_queryset()
19 |
20 | def test_me(self, user: User, rf: RequestFactory):
21 | view = UserViewSet()
22 | request = rf.get("/fake-url/")
23 | request.user = user
24 |
25 | view.request = request
26 |
27 | response = view.me(request)
28 |
29 | assert response.data == {
30 | "username": user.username,
31 | "name": user.name,
32 | "url": f"http://testserver/api/users/{user.username}/",
33 | }
34 |
--------------------------------------------------------------------------------
/djsniper/users/tests/test_forms.py:
--------------------------------------------------------------------------------
1 | """
2 | Module for all Form Tests.
3 | """
4 | import pytest
5 | from django.utils.translation import gettext_lazy as _
6 |
7 | from djsniper.users.forms import UserCreationForm
8 | from djsniper.users.models import User
9 |
10 | pytestmark = pytest.mark.django_db
11 |
12 |
13 | class TestUserCreationForm:
14 | """
15 | Test class for all tests related to the UserCreationForm
16 | """
17 |
18 | def test_username_validation_error_msg(self, user: User):
19 | """
20 | Tests UserCreation Form's unique validator functions correctly by testing:
21 | 1) A new user with an existing username cannot be added.
22 | 2) Only 1 error is raised by the UserCreation Form
23 | 3) The desired error message is raised
24 | """
25 |
26 | # The user already exists,
27 | # hence cannot be created.
28 | form = UserCreationForm(
29 | {
30 | "username": user.username,
31 | "password1": user.password,
32 | "password2": user.password,
33 | }
34 | )
35 |
36 | assert not form.is_valid()
37 | assert len(form.errors) == 1
38 | assert "username" in form.errors
39 | assert form.errors["username"][0] == _("This username has already been taken.")
40 |
--------------------------------------------------------------------------------
/djsniper/users/tests/test_models.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from djsniper.users.models import User
4 |
5 | pytestmark = pytest.mark.django_db
6 |
7 |
8 | def test_user_get_absolute_url(user: User):
9 | assert user.get_absolute_url() == f"/users/{user.username}/"
10 |
--------------------------------------------------------------------------------
/djsniper/users/tests/test_tasks.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from celery.result import EagerResult
3 |
4 | from djsniper.users.tasks import get_users_count
5 | from djsniper.users.tests.factories import UserFactory
6 |
7 | pytestmark = pytest.mark.django_db
8 |
9 |
10 | def test_user_count(settings):
11 | """A basic test to execute the get_users_count Celery task."""
12 | UserFactory.create_batch(3)
13 | settings.CELERY_TASK_ALWAYS_EAGER = True
14 | task_result = get_users_count.delay()
15 | assert isinstance(task_result, EagerResult)
16 | assert task_result.result == 3
17 |
--------------------------------------------------------------------------------
/djsniper/users/tests/test_urls.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.urls import resolve, reverse
3 |
4 | from djsniper.users.models import User
5 |
6 | pytestmark = pytest.mark.django_db
7 |
8 |
9 | def test_detail(user: User):
10 | assert (
11 | reverse("users:detail", kwargs={"username": user.username})
12 | == f"/users/{user.username}/"
13 | )
14 | assert resolve(f"/users/{user.username}/").view_name == "users:detail"
15 |
16 |
17 | def test_update():
18 | assert reverse("users:update") == "/users/~update/"
19 | assert resolve("/users/~update/").view_name == "users:update"
20 |
21 |
22 | def test_redirect():
23 | assert reverse("users:redirect") == "/users/~redirect/"
24 | assert resolve("/users/~redirect/").view_name == "users:redirect"
25 |
--------------------------------------------------------------------------------
/djsniper/users/tests/test_views.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.conf import settings
3 | from django.contrib import messages
4 | from django.contrib.auth.models import AnonymousUser
5 | from django.contrib.messages.middleware import MessageMiddleware
6 | from django.contrib.sessions.middleware import SessionMiddleware
7 | from django.http import HttpRequest, HttpResponseRedirect
8 | from django.test import RequestFactory
9 | from django.urls import reverse
10 |
11 | from djsniper.users.forms import UserChangeForm
12 | from djsniper.users.models import User
13 | from djsniper.users.tests.factories import UserFactory
14 | from djsniper.users.views import (
15 | UserRedirectView,
16 | UserUpdateView,
17 | user_detail_view,
18 | )
19 |
20 | pytestmark = pytest.mark.django_db
21 |
22 |
23 | class TestUserUpdateView:
24 | """
25 | TODO:
26 | extracting view initialization code as class-scoped fixture
27 | would be great if only pytest-django supported non-function-scoped
28 | fixture db access -- this is a work-in-progress for now:
29 | https://github.com/pytest-dev/pytest-django/pull/258
30 | """
31 |
32 | def dummy_get_response(self, request: HttpRequest):
33 | return None
34 |
35 | def test_get_success_url(self, user: User, rf: RequestFactory):
36 | view = UserUpdateView()
37 | request = rf.get("/fake-url/")
38 | request.user = user
39 |
40 | view.request = request
41 |
42 | assert view.get_success_url() == f"/users/{user.username}/"
43 |
44 | def test_get_object(self, user: User, rf: RequestFactory):
45 | view = UserUpdateView()
46 | request = rf.get("/fake-url/")
47 | request.user = user
48 |
49 | view.request = request
50 |
51 | assert view.get_object() == user
52 |
53 | def test_form_valid(self, user: User, rf: RequestFactory):
54 | view = UserUpdateView()
55 | request = rf.get("/fake-url/")
56 |
57 | # Add the session/message middleware to the request
58 | SessionMiddleware(self.dummy_get_response).process_request(request)
59 | MessageMiddleware(self.dummy_get_response).process_request(request)
60 | request.user = user
61 |
62 | view.request = request
63 |
64 | # Initialize the form
65 | form = UserChangeForm()
66 | form.cleaned_data = []
67 | view.form_valid(form)
68 |
69 | messages_sent = [m.message for m in messages.get_messages(request)]
70 | assert messages_sent == ["Information successfully updated"]
71 |
72 |
73 | class TestUserRedirectView:
74 | def test_get_redirect_url(self, user: User, rf: RequestFactory):
75 | view = UserRedirectView()
76 | request = rf.get("/fake-url")
77 | request.user = user
78 |
79 | view.request = request
80 |
81 | assert view.get_redirect_url() == f"/users/{user.username}/"
82 |
83 |
84 | class TestUserDetailView:
85 | def test_authenticated(self, user: User, rf: RequestFactory):
86 | request = rf.get("/fake-url/")
87 | request.user = UserFactory()
88 |
89 | response = user_detail_view(request, username=user.username)
90 |
91 | assert response.status_code == 200
92 |
93 | def test_not_authenticated(self, user: User, rf: RequestFactory):
94 | request = rf.get("/fake-url/")
95 | request.user = AnonymousUser()
96 |
97 | response = user_detail_view(request, username=user.username)
98 | login_url = reverse(settings.LOGIN_URL)
99 |
100 | assert isinstance(response, HttpResponseRedirect)
101 | assert response.status_code == 302
102 | assert response.url == f"{login_url}?next=/fake-url/"
103 |
--------------------------------------------------------------------------------
/djsniper/users/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from djsniper.users.views import (
4 | user_detail_view,
5 | user_redirect_view,
6 | user_update_view,
7 | )
8 |
9 | app_name = "users"
10 | urlpatterns = [
11 | path("~redirect/", view=user_redirect_view, name="redirect"),
12 | path("~update/", view=user_update_view, name="update"),
13 | path("/", view=user_detail_view, name="detail"),
14 | ]
15 |
--------------------------------------------------------------------------------
/djsniper/users/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 | from django.contrib.auth.mixins import LoginRequiredMixin
3 | from django.contrib.messages.views import SuccessMessageMixin
4 | from django.urls import reverse
5 | from django.utils.translation import gettext_lazy as _
6 | from django.views.generic import DetailView, RedirectView, UpdateView
7 |
8 | User = get_user_model()
9 |
10 |
11 | class UserDetailView(LoginRequiredMixin, DetailView):
12 |
13 | model = User
14 | slug_field = "username"
15 | slug_url_kwarg = "username"
16 |
17 |
18 | user_detail_view = UserDetailView.as_view()
19 |
20 |
21 | class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
22 |
23 | model = User
24 | fields = ["name"]
25 | success_message = _("Information successfully updated")
26 |
27 | def get_success_url(self):
28 | assert (
29 | self.request.user.is_authenticated
30 | ) # for mypy to know that the user is authenticated
31 | return self.request.user.get_absolute_url()
32 |
33 | def get_object(self):
34 | return self.request.user
35 |
36 |
37 | user_update_view = UserUpdateView.as_view()
38 |
39 |
40 | class UserRedirectView(LoginRequiredMixin, RedirectView):
41 |
42 | permanent = False
43 |
44 | def get_redirect_url(self):
45 | return reverse("users:detail", kwargs={"username": self.request.user.username})
46 |
47 |
48 | user_redirect_view = UserRedirectView.as_view()
49 |
--------------------------------------------------------------------------------
/djsniper/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/utils/__init__.py
--------------------------------------------------------------------------------
/djsniper/utils/storages.py:
--------------------------------------------------------------------------------
1 | from storages.backends.s3boto3 import S3Boto3Storage
2 |
3 |
4 | class StaticRootS3Boto3Storage(S3Boto3Storage):
5 | location = "static"
6 | default_acl = "public-read"
7 |
8 |
9 | class MediaRootS3Boto3Storage(S3Boto3Storage):
10 | location = "media"
11 | file_overwrite = False
12 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = ./_build
10 | APP = /app
11 |
12 | .PHONY: help livehtml apidocs Makefile
13 |
14 | # Put it first so that "make" without argument is like "make help".
15 | help:
16 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -c .
17 |
18 | # Build, watch and serve docs with live reload
19 | livehtml:
20 | sphinx-autobuild -b html --host 0.0.0.0 --port 7000 --watch $(APP) -c . $(SOURCEDIR) $(BUILDDIR)/html
21 |
22 | # Outputs rst files from django application code
23 | apidocs:
24 | sphinx-apidoc -o $(SOURCEDIR)/api $(APP)
25 |
26 | # Catch-all target: route all unknown targets to Sphinx using the new
27 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
28 | %: Makefile
29 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -c .
30 |
--------------------------------------------------------------------------------
/docs/__init__.py:
--------------------------------------------------------------------------------
1 | # Included so that Django's startproject comment runs against the docs directory
2 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 |
13 | import os
14 | import sys
15 | import django
16 |
17 | if os.getenv("READTHEDOCS", default=False) == "True":
18 | sys.path.insert(0, os.path.abspath(".."))
19 | os.environ["DJANGO_READ_DOT_ENV_FILE"] = "True"
20 | os.environ["USE_DOCKER"] = "no"
21 | else:
22 | sys.path.insert(0, os.path.abspath("/app"))
23 | os.environ["DATABASE_URL"] = "sqlite:///readthedocs.db"
24 | os.environ["CELERY_BROKER_URL"] = os.getenv("REDIS_URL", "redis://redis:6379")
25 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
26 | django.setup()
27 |
28 | # -- Project information -----------------------------------------------------
29 |
30 | project = "djsniper"
31 | copyright = """2021, Matthew Freire"""
32 | author = "Matthew Freire"
33 |
34 |
35 | # -- General configuration ---------------------------------------------------
36 |
37 | # Add any Sphinx extension module names here, as strings. They can be
38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
39 | # ones.
40 | extensions = [
41 | "sphinx.ext.autodoc",
42 | "sphinx.ext.napoleon",
43 | ]
44 |
45 | # Add any paths that contain templates here, relative to this directory.
46 | # templates_path = ["_templates"]
47 |
48 | # List of patterns, relative to source directory, that match files and
49 | # directories to ignore when looking for source files.
50 | # This pattern also affects html_static_path and html_extra_path.
51 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
52 |
53 | # -- Options for HTML output -------------------------------------------------
54 |
55 | # The theme to use for HTML and HTML Help pages. See the documentation for
56 | # a list of builtin themes.
57 | #
58 | html_theme = "alabaster"
59 |
60 | # Add any paths that contain custom static files (such as style sheets) here,
61 | # relative to this directory. They are copied after the builtin static files,
62 | # so a file named "default.css" will overwrite the builtin "default.css".
63 | # html_static_path = ["_static"]
64 |
--------------------------------------------------------------------------------
/docs/howto.rst:
--------------------------------------------------------------------------------
1 | How To - Project Documentation
2 | ======================================================================
3 |
4 | Get Started
5 | ----------------------------------------------------------------------
6 |
7 | Documentation can be written as rst files in `djsniper/docs`.
8 |
9 |
10 | To build and serve docs, use the commands::
11 |
12 | docker-compose -f local.yml up docs
13 |
14 |
15 |
16 | Changes to files in `docs/_source` will be picked up and reloaded automatically.
17 |
18 | `Sphinx `_ is the tool used to build documentation.
19 |
20 | Docstrings to Documentation
21 | ----------------------------------------------------------------------
22 |
23 | The sphinx extension `apidoc `_ is used to automatically document code using signatures and docstrings.
24 |
25 | Numpy or Google style docstrings will be picked up from project files and availble for documentation. See the `Napoleon `_ extension for details.
26 |
27 | For an in-use example, see the `page source <_sources/users.rst.txt>`_ for :ref:`users`.
28 |
29 | To compile all docstrings automatically into documentation source files, use the command:
30 | ::
31 |
32 | make apidocs
33 |
34 |
35 | This can be done in the docker container:
36 | ::
37 |
38 | docker run --rm docs make apidocs
39 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. djsniper documentation master file, created by
2 | sphinx-quickstart.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to djsniper's documentation!
7 | ======================================================================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 | :caption: Contents:
12 |
13 | howto
14 | pycharm/configuration
15 | users
16 |
17 |
18 |
19 | Indices and tables
20 | ==================
21 |
22 | * :ref:`genindex`
23 | * :ref:`modindex`
24 | * :ref:`search`
25 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 |
8 | if "%SPHINXBUILD%" == "" (
9 | set SPHINXBUILD=sphinx-build -c .
10 | )
11 | set SOURCEDIR=_source
12 | set BUILDDIR=_build
13 | set APP=..\djsniper
14 |
15 | if "%1" == "" goto help
16 |
17 | %SPHINXBUILD% >NUL 2>NUL
18 | if errorlevel 9009 (
19 | echo.
20 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
21 | echo.installed, then set the SPHINXBUILD environment variable to point
22 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
23 | echo.may add the Sphinx directory to PATH.
24 | echo.
25 | echo.Install sphinx-autobuild for live serving.
26 | echo.If you don't have Sphinx installed, grab it from
27 | echo.http://sphinx-doc.org/
28 | exit /b 1
29 | )
30 |
31 | %SPHINXBUILD% -b %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
32 | goto end
33 |
34 | :livehtml
35 | sphinx-autobuild -b html --open-browser -p 7000 --watch %APP% -c . %SOURCEDIR% %BUILDDIR%/html
36 | GOTO :EOF
37 |
38 | :apidocs
39 | sphinx-apidoc -o %SOURCEDIR%/api %APP%
40 | GOTO :EOF
41 |
42 | :help
43 | %SPHINXBUILD% -b help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
44 |
45 | :end
46 | popd
47 |
--------------------------------------------------------------------------------
/docs/pycharm/configuration.rst:
--------------------------------------------------------------------------------
1 | Docker Remote Debugging
2 | =======================
3 |
4 | To connect to python remote interpreter inside docker, you have to make sure first, that Pycharm is aware of your docker.
5 |
6 | Go to *Settings > Build, Execution, Deployment > Docker*. If you are on linux, you can use docker directly using its socket `unix:///var/run/docker.sock`, if you are on Windows or Mac, make sure that you have docker-machine installed, then you can simply *Import credentials from Docker Machine*.
7 |
8 | .. image:: images/1.png
9 |
10 | Configure Remote Python Interpreter
11 | -----------------------------------
12 |
13 | This repository comes with already prepared "Run/Debug Configurations" for docker.
14 |
15 | .. image:: images/2.png
16 |
17 | But as you can see, at the beginning there is something wrong with them. They have red X on django icon, and they cannot be used, without configuring remote python interpreter. To do that, you have to go to *Settings > Build, Execution, Deployment* first.
18 |
19 |
20 | Next, you have to add new remote python interpreter, based on already tested deployment settings. Go to *Settings > Project > Project Interpreter*. Click on the cog icon, and click *Add Remote*.
21 |
22 | .. image:: images/3.png
23 |
24 | Switch to *Docker Compose* and select `local.yml` file from directory of your project, next set *Service name* to `django`
25 |
26 | .. image:: images/4.png
27 |
28 | Having that, click *OK*. Close *Settings* panel, and wait few seconds...
29 |
30 | .. image:: images/7.png
31 |
32 | After few seconds, all *Run/Debug Configurations* should be ready to use.
33 |
34 | .. image:: images/8.png
35 |
36 | **Things you can do with provided configuration**:
37 |
38 | * run and debug python code
39 |
40 | .. image:: images/f1.png
41 |
42 | * run and debug tests
43 |
44 | .. image:: images/f2.png
45 | .. image:: images/f3.png
46 |
47 | * run and debug migrations or different django management commands
48 |
49 | .. image:: images/f4.png
50 |
51 | * and many others..
52 |
53 | Known issues
54 | ------------
55 |
56 | * Pycharm hangs on "Connecting to Debugger"
57 |
58 | .. image:: images/issue1.png
59 |
60 | This might be fault of your firewall. Take a look on this ticket - https://youtrack.jetbrains.com/issue/PY-18913
61 |
62 | * Modified files in `.idea` directory
63 |
64 | Most of the files from `.idea/` were added to `.gitignore` with a few exceptions, which were made, to provide "ready to go" configuration. After adding remote interpreter some of these files are altered by PyCharm:
65 |
66 | .. image:: images/issue2.png
67 |
68 | In theory you can remove them from repository, but then, other people will lose a ability to initialize a project from provided configurations as you did. To get rid of this annoying state, you can run command::
69 |
70 | $ git update-index --assume-unchanged djsniper.iml
71 |
--------------------------------------------------------------------------------
/docs/pycharm/images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/1.png
--------------------------------------------------------------------------------
/docs/pycharm/images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/2.png
--------------------------------------------------------------------------------
/docs/pycharm/images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/3.png
--------------------------------------------------------------------------------
/docs/pycharm/images/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/4.png
--------------------------------------------------------------------------------
/docs/pycharm/images/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/7.png
--------------------------------------------------------------------------------
/docs/pycharm/images/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/8.png
--------------------------------------------------------------------------------
/docs/pycharm/images/f1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/f1.png
--------------------------------------------------------------------------------
/docs/pycharm/images/f2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/f2.png
--------------------------------------------------------------------------------
/docs/pycharm/images/f3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/f3.png
--------------------------------------------------------------------------------
/docs/pycharm/images/f4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/f4.png
--------------------------------------------------------------------------------
/docs/pycharm/images/issue1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/issue1.png
--------------------------------------------------------------------------------
/docs/pycharm/images/issue2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/issue2.png
--------------------------------------------------------------------------------
/docs/users.rst:
--------------------------------------------------------------------------------
1 | .. _users:
2 |
3 | Users
4 | ======================================================================
5 |
6 | Starting a new project, it’s highly recommended to set up a custom user model,
7 | even if the default User model is sufficient for you.
8 |
9 | This model behaves identically to the default user model,
10 | but you’ll be able to customize it in the future if the need arises.
11 |
12 | .. automodule:: djsniper.users.models
13 | :members:
14 | :noindex:
15 |
16 |
--------------------------------------------------------------------------------
/local.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | volumes:
4 | local_postgres_data: {}
5 | local_postgres_data_backups: {}
6 |
7 | services:
8 | django: &django
9 | build:
10 | context: .
11 | dockerfile: ./compose/local/django/Dockerfile
12 | image: djsniper_local_django
13 | container_name: sniper_django
14 | depends_on:
15 | - postgres
16 | - redis
17 | volumes:
18 | - .:/app:z
19 | env_file:
20 | - ./.envs/.local/.django
21 | - ./.envs/.local/.postgres
22 | ports:
23 | - "8000:8000"
24 | command: /start
25 |
26 | postgres:
27 | build:
28 | context: .
29 | dockerfile: ./compose/production/postgres/Dockerfile
30 | image: djsniper_production_postgres
31 | container_name: sniper_postgres
32 | volumes:
33 | - local_postgres_data:/var/lib/postgresql/data:Z
34 | - local_postgres_data_backups:/backups:z
35 | env_file:
36 | - ./.envs/.local/.postgres
37 |
38 | # docs:
39 | # image: djsniper_local_docs
40 | # container_name: sniper_docs
41 | # build:
42 | # context: .
43 | # dockerfile: ./compose/local/docs/Dockerfile
44 | # env_file:
45 | # - ./.envs/.local/.django
46 | # volumes:
47 | # - ./docs:/docs:z
48 | # - ./config:/app/config:z
49 | # - ./djsniper:/app/djsniper:z
50 | # ports:
51 | # - "7000:7000"
52 | # command: /start-docs
53 |
54 | redis:
55 | image: redis:6
56 | container_name: sniper_redis
57 |
58 | celeryworker:
59 | <<: *django
60 | image: djsniper_local_celeryworker
61 | container_name: sniper_celeryworker
62 | depends_on:
63 | - redis
64 | - postgres
65 | ports: []
66 | command: /start-celeryworker
67 |
68 | celerybeat:
69 | <<: *django
70 | image: djsniper_local_celerybeat
71 | container_name: sniper_celerybeat
72 | depends_on:
73 | - redis
74 | - postgres
75 | ports: []
76 | command: /start-celerybeat
77 |
78 | flower:
79 | <<: *django
80 | image: djsniper_local_flower
81 | container_name: sniper_flower
82 | ports:
83 | - "5555:5555"
84 | command: /start-flower
85 |
--------------------------------------------------------------------------------
/locale/README.rst:
--------------------------------------------------------------------------------
1 | Translations
2 | ============
3 |
4 | Translations will be placed in this folder when running::
5 |
6 | python manage.py makemessages
7 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 | from pathlib import Path
5 |
6 | if __name__ == "__main__":
7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
8 |
9 | try:
10 | from django.core.management import execute_from_command_line
11 | except ImportError:
12 | # The above import may fail for some other reason. Ensure that the
13 | # issue is really that Django is missing to avoid masking other
14 | # exceptions on Python 2.
15 | try:
16 | import django # noqa
17 | except ImportError:
18 | raise ImportError(
19 | "Couldn't import Django. Are you sure it's installed and "
20 | "available on your PYTHONPATH environment variable? Did you "
21 | "forget to activate a virtual environment?"
22 | )
23 |
24 | raise
25 |
26 | # This allows easy placement of apps within the interior
27 | # djsniper directory.
28 | current_path = Path(__file__).parent.resolve()
29 | sys.path.append(str(current_path / "djsniper"))
30 |
31 | execute_from_command_line(sys.argv)
32 |
--------------------------------------------------------------------------------
/merge_production_dotenvs_in_dotenv.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 | from typing import Sequence
4 |
5 | import pytest
6 |
7 | ROOT_DIR_PATH = Path(__file__).parent.resolve()
8 | PRODUCTION_DOTENVS_DIR_PATH = ROOT_DIR_PATH / ".envs" / ".production"
9 | PRODUCTION_DOTENV_FILE_PATHS = [
10 | PRODUCTION_DOTENVS_DIR_PATH / ".django",
11 | PRODUCTION_DOTENVS_DIR_PATH / ".postgres",
12 | ]
13 | DOTENV_FILE_PATH = ROOT_DIR_PATH / ".env"
14 |
15 |
16 | def merge(
17 | output_file_path: str, merged_file_paths: Sequence[str], append_linesep: bool = True
18 | ) -> None:
19 | with open(output_file_path, "w") as output_file:
20 | for merged_file_path in merged_file_paths:
21 | with open(merged_file_path, "r") as merged_file:
22 | merged_file_content = merged_file.read()
23 | output_file.write(merged_file_content)
24 | if append_linesep:
25 | output_file.write(os.linesep)
26 |
27 |
28 | def main():
29 | merge(DOTENV_FILE_PATH, PRODUCTION_DOTENV_FILE_PATHS)
30 |
31 |
32 | @pytest.mark.parametrize("merged_file_count", range(3))
33 | @pytest.mark.parametrize("append_linesep", [True, False])
34 | def test_merge(tmpdir_factory, merged_file_count: int, append_linesep: bool):
35 | tmp_dir_path = Path(str(tmpdir_factory.getbasetemp()))
36 |
37 | output_file_path = tmp_dir_path / ".env"
38 |
39 | expected_output_file_content = ""
40 | merged_file_paths = []
41 | for i in range(merged_file_count):
42 | merged_file_ord = i + 1
43 |
44 | merged_filename = ".service{}".format(merged_file_ord)
45 | merged_file_path = tmp_dir_path / merged_filename
46 |
47 | merged_file_content = merged_filename * merged_file_ord
48 |
49 | with open(merged_file_path, "w+") as file:
50 | file.write(merged_file_content)
51 |
52 | expected_output_file_content += merged_file_content
53 | if append_linesep:
54 | expected_output_file_content += os.linesep
55 |
56 | merged_file_paths.append(merged_file_path)
57 |
58 | merge(output_file_path, merged_file_paths, append_linesep)
59 |
60 | with open(output_file_path, "r") as output_file:
61 | actual_output_file_content = output_file.read()
62 |
63 | assert actual_output_file_content == expected_output_file_content
64 |
65 |
66 | if __name__ == "__main__":
67 | main()
68 |
--------------------------------------------------------------------------------
/production.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | volumes:
4 | production_postgres_data: {}
5 | production_postgres_data_backups: {}
6 | production_traefik: {}
7 |
8 | services:
9 | django: &django
10 | build:
11 | context: .
12 | dockerfile: ./compose/production/django/Dockerfile
13 | image: djsniper_production_django
14 | depends_on:
15 | - postgres
16 | - redis
17 | env_file:
18 | - ./.envs/.production/.django
19 | - ./.envs/.production/.postgres
20 | command: /start
21 |
22 | postgres:
23 | build:
24 | context: .
25 | dockerfile: ./compose/production/postgres/Dockerfile
26 | image: djsniper_production_postgres
27 | volumes:
28 | - production_postgres_data:/var/lib/postgresql/data:Z
29 | - production_postgres_data_backups:/backups:z
30 | env_file:
31 | - ./.envs/.production/.postgres
32 |
33 | traefik:
34 | build:
35 | context: .
36 | dockerfile: ./compose/production/traefik/Dockerfile
37 | image: djsniper_production_traefik
38 | depends_on:
39 | - django
40 | volumes:
41 | - production_traefik:/etc/traefik/acme:z
42 | ports:
43 | - "0.0.0.0:80:80"
44 | - "0.0.0.0:443:443"
45 | - "0.0.0.0:5555:5555"
46 |
47 | redis:
48 | image: redis:6
49 |
50 | celeryworker:
51 | <<: *django
52 | image: djsniper_production_celeryworker
53 | command: /start-celeryworker
54 |
55 | celerybeat:
56 | <<: *django
57 | image: djsniper_production_celerybeat
58 | command: /start-celerybeat
59 |
60 | flower:
61 | <<: *django
62 | image: djsniper_production_flower
63 | command: /start-flower
64 |
65 | awscli:
66 | build:
67 | context: .
68 | dockerfile: ./compose/production/aws/Dockerfile
69 | env_file:
70 | - ./.envs/.production/.django
71 | volumes:
72 | - production_postgres_data_backups:/backups:z
73 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = --ds=config.settings.test --reuse-db
3 | python_files = tests.py test_*.py
4 |
--------------------------------------------------------------------------------
/requirements/base.txt:
--------------------------------------------------------------------------------
1 | pytz==2021.3 # https://github.com/stub42/pytz
2 | python-slugify==5.0.2 # https://github.com/un33k/python-slugify
3 | Pillow==8.4.0 # https://github.com/python-pillow/Pillow
4 | argon2-cffi==21.1.0 # https://github.com/hynek/argon2_cffi
5 | redis==3.5.3 # https://github.com/redis/redis-py
6 | hiredis==2.0.0 # https://github.com/redis/hiredis-py
7 | celery==5.2.1 # pyup: < 6.0 # https://github.com/celery/celery
8 | django-celery-beat==2.2.1 # https://github.com/celery/django-celery-beat
9 | flower==1.0.0 # https://github.com/mher/flower
10 | web3==5.25.0 # https://github.com/ethereum/web3.py
11 |
12 | # Django
13 | # ------------------------------------------------------------------------------
14 | django==3.2.9 # pyup: < 4.0 # https://www.djangoproject.com/
15 | django-environ==0.8.1 # https://github.com/joke2k/django-environ
16 | django-model-utils==4.2.0 # https://github.com/jazzband/django-model-utils
17 | django-allauth==0.46.0 # https://github.com/pennersr/django-allauth
18 | django-crispy-forms==1.13.0 # https://github.com/django-crispy-forms/django-crispy-forms
19 | crispy-bootstrap5==0.6 # https://github.com/django-crispy-forms/crispy-bootstrap5
20 | django-redis==5.1.0 # https://github.com/jazzband/django-redis
21 | # Django REST Framework
22 | djangorestframework==3.12.4 # https://github.com/encode/django-rest-framework
23 | django-cors-headers==3.10.1 # https://github.com/adamchainz/django-cors-headers
24 | crispy-tailwind==0.5.0 # https://github.com/django-crispy-forms/crispy-tailwind
25 | celery-progress==0.1.1
--------------------------------------------------------------------------------
/requirements/local.txt:
--------------------------------------------------------------------------------
1 | -r base.txt
2 |
3 | Werkzeug==2.0.2 # https://github.com/pallets/werkzeug
4 | ipdb==0.13.9 # https://github.com/gotcha/ipdb
5 | psycopg2==2.9.2 # https://github.com/psycopg/psycopg2
6 | watchgod==0.7 # https://github.com/samuelcolvin/watchgod
7 |
8 | # Testing
9 | # ------------------------------------------------------------------------------
10 | mypy==0.910 # https://github.com/python/mypy
11 | django-stubs==1.9.0 # https://github.com/typeddjango/django-stubs
12 | pytest==6.2.5 # https://github.com/pytest-dev/pytest
13 | pytest-sugar==0.9.4 # https://github.com/Frozenball/pytest-sugar
14 | djangorestframework-stubs==1.4.0 # https://github.com/typeddjango/djangorestframework-stubs
15 |
16 | # Documentation
17 | # ------------------------------------------------------------------------------
18 | sphinx==4.3.1 # https://github.com/sphinx-doc/sphinx
19 | sphinx-autobuild==2021.3.14 # https://github.com/GaretJax/sphinx-autobuild
20 |
21 | # Code quality
22 | # ------------------------------------------------------------------------------
23 | flake8==4.0.1 # https://github.com/PyCQA/flake8
24 | flake8-isort==4.1.1 # https://github.com/gforcada/flake8-isort
25 | coverage==6.2 # https://github.com/nedbat/coveragepy
26 | black==21.12b0 # https://github.com/psf/black
27 | pylint-django==2.4.4 # https://github.com/PyCQA/pylint-django
28 | pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery
29 | pre-commit==2.16.0 # https://github.com/pre-commit/pre-commit
30 |
31 | # Django
32 | # ------------------------------------------------------------------------------
33 | factory-boy==3.2.1 # https://github.com/FactoryBoy/factory_boy
34 |
35 | django-debug-toolbar==3.2.2 # https://github.com/jazzband/django-debug-toolbar
36 | django-extensions==3.1.5 # https://github.com/django-extensions/django-extensions
37 | django-coverage-plugin==2.0.2 # https://github.com/nedbat/django_coverage_plugin
38 | pytest-django==4.5.1 # https://github.com/pytest-dev/pytest-django
39 |
--------------------------------------------------------------------------------
/requirements/production.txt:
--------------------------------------------------------------------------------
1 | # PRECAUTION: avoid production dependencies that aren't in development
2 |
3 | -r base.txt
4 |
5 | gunicorn==20.1.0 # https://github.com/benoitc/gunicorn
6 | psycopg2==2.9.2 # https://github.com/psycopg/psycopg2
7 | Collectfast==2.2.0 # https://github.com/antonagestam/collectfast
8 | sentry-sdk==1.5.0 # https://github.com/getsentry/sentry-python
9 |
10 | # Django
11 | # ------------------------------------------------------------------------------
12 | django-storages[boto3]==1.12.3 # https://github.com/jschneier/django-storages
13 | django-anymail[mailgun]==8.4 # https://github.com/anymail/django-anymail
14 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 120
3 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
4 |
5 | [pycodestyle]
6 | max-line-length = 120
7 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
8 |
9 | [isort]
10 | line_length = 88
11 | known_first_party = djsniper,config
12 | multi_line_output = 3
13 | default_section = THIRDPARTY
14 | skip = venv/
15 | skip_glob = **/migrations/*.py
16 | include_trailing_comma = true
17 | force_grid_wrap = 0
18 | use_parentheses = true
19 |
20 | [mypy]
21 | python_version = 3.9
22 | check_untyped_defs = True
23 | ignore_missing_imports = True
24 | warn_unused_ignores = True
25 | warn_redundant_casts = True
26 | warn_unused_configs = True
27 | plugins = mypy_django_plugin.main, mypy_drf_plugin.main
28 |
29 | [mypy.plugins.django-stubs]
30 | django_settings_module = config.settings.test
31 |
32 | [mypy-*.migrations.*]
33 | # Django migrations should not produce any errors:
34 | ignore_errors = True
35 |
36 | [coverage:run]
37 | include = djsniper/*
38 | omit = *migrations*, *tests*
39 | plugins =
40 | django_coverage_plugin
41 |
--------------------------------------------------------------------------------